Learning Goals
3 minBy the end of this lesson you can:
- Build a Queue class with enqueue, dequeue, peek, size.
- Understand why a plain list makes a slow queue — and why
collections.dequeis the right tool. - Apply queues to print spoolers and task scheduling.
- Implement a simple BFS using a queue.
Warm-Up · The Cinema Queue
5 minStand at the back. People in front of you get served first. New people join the back. That's a queue. FIFO — first in, first out.
enqueue A → [A] (joined the back) enqueue B → [A, B] enqueue C → [A, B, C] dequeue → returns A; [B, C] (A leaves the front) dequeue → returns B; [C] peek → returns C (look at front)
A queue is a stack's cousin — same shape, opposite ends. Add at one end; remove from the other. Order preserved.
New Concept · The Queue API
14 minThe naïve list version (and why it's wrong)
# DON'T do this for production queues class SlowQueue: def __init__(self): self._items = [] def enqueue(self, item): self._items.append(item) # O(1) — fast def dequeue(self): return self._items.pop(0) # O(n) — SLOW! shifts everything left # A million enqueues and dequeues = a trillion total operations
list.pop(0) has to shift every other element left by one slot. That makes the queue O(n) per dequeue — quadratic across many operations. For tiny queues it's fine; for production it's a hidden disaster.
The right answer · collections.deque
from collections import deque q = deque() q.append("A") # enqueue — O(1) q.append("B") q.append("C") print(q.popleft()) # dequeue — O(1)! returns "A" print(q[0]) # peek — O(1) print(len(q)) # size — O(1)
deque (double-ended queue) is a doubly-linked list under the hood. Both ends are O(1). It's the standard Python queue.
A wrapper class for clarity
from collections import deque class Queue: def __init__(self): self._items = deque() def enqueue(self, item): self._items.append(item) def dequeue(self): if not self._items: raise IndexError("dequeue from empty queue") return self._items.popleft() def peek(self): if not self._items: raise IndexError("peek into empty queue") return self._items[0] def __len__(self): return len(self._items) def is_empty(self): return len(self._items) == 0 def __repr__(self): return f"Queue(front → {list(self._items)})"
Application 1 · Print spooler
Multiple users send print jobs to a shared printer. The printer can only process one at a time. Jobs are processed in order received — perfect FIFO.
printer_queue = Queue() printer_queue.enqueue(("Aisyah", "report.pdf", 3)) printer_queue.enqueue(("Wei Jie", "essay.docx", 10)) printer_queue.enqueue(("Priya", "photo.png", 1)) while not printer_queue.is_empty(): user, file, pages = printer_queue.dequeue() print(f"Printing {file} for {user} ({pages} pages)")
Application 2 · Task scheduler
A program processes tasks one at a time. New tasks join the back; the worker pulls from the front.
tasks = Queue() def schedule(task): tasks.enqueue(task) def worker(): while not tasks.is_empty(): task = tasks.dequeue() print(f"Doing: {task}")
Application 3 · Breadth-first search
BFS uses a queue to explore graph nodes level by level. Compare to depth-first (which uses a stack — explicit or via recursion).
def bfs(graph, start): """Visit every reachable node, breadth-first.""" visited = set() q = deque([start]) while q: node = q.popleft() if node in visited: continue visited.add(node) print(f" visit {node}") for neighbour in graph[node]: q.append(neighbour) return visited graph = { "A": ["B", "C"], "B": ["D", "E"], "C": ["F"], "D": [], "E": ["F"], "F": [], } bfs(graph, "A") # visit A # visit B # visit C # visit D # visit E # visit F
BFS is the standard way to find shortest paths in unweighted graphs, "degrees of separation", level-by-level explorations.
Worked Example · The Bus Stop
12 minSimulate a bus stop. Passengers arrive at random times; the bus picks up the front 5 every 10 minutes. Save as bus_stop.py:
# bus_stop.py — FIFO queue simulation from collections import deque import random def simulate(minutes=60, arrival_rate=0.8, bus_capacity=5, bus_interval=10): """One passenger arrives with arrival_rate probability each minute. A bus comes every bus_interval minutes and takes the front N.""" queue = deque() served = 0 max_queue = 0 for minute in range(minutes): # Possibly add a passenger if random.random() < arrival_rate: arrived_at = minute queue.append(arrived_at) max_queue = max(max_queue, len(queue)) # Bus? if (minute + 1) % bus_interval == 0: taking = min(bus_capacity, len(queue)) for _ in range(taking): queue.popleft() served += taking print(f" min {minute + 1:>3}: bus takes {taking}; " f"queue length now {len(queue)}") print(f"\n=== Summary ===") print(f"Total served: {served}") print(f"Max queue length: {max_queue}") print(f"Still waiting: {len(queue)}") random.seed(42) simulate()
Sample output
min 10: bus takes 5; queue length now 4 min 20: bus takes 5; queue length now 3 min 30: bus takes 5; queue length now 2 min 40: bus takes 5; queue length now 5 min 50: bus takes 5; queue length now 3 min 60: bus takes 5; queue length now 2 === Summary === Total served: 30 Max queue length: 9 Still waiting: 2
Read the diff
One deque models the entire queue. Per-minute simulation: arrival (sometimes); bus (every 10 mins) takes up to capacity. The data structure naturally enforces FIFO — first arrivals get on first.
Try It Yourself
13 minN people stand in a circle. Pass the potato K times; whoever holds it on the Kth pass is out. Continue until one remains. Use a queue to model the circle.
Hint
from collections import deque def hot_potato(names, k): q = deque(names) while len(q) > 1: for _ in range(k): q.append(q.popleft()) # rotate out = q.popleft() print(f"Eliminated: {out}") return q[0] print(hot_potato(["Aisyah", "Wei Jie", "Priya", "Iman", "Aizat"], 3))
Rotating with append(popleft()) is the canonical queue circle trick. Notice this is the classic Josephus problem.
Compare 100,000 enqueue+dequeue operations on a list (using pop(0)) vs on a deque. The deque should be many times faster.
Hint
from collections import deque import time N = 100_000 # list-based queue q1 = [] start = time.perf_counter() for i in range(N): q1.append(i) for _ in range(N): q1.pop(0) print(f"list: {(time.perf_counter() - start) * 1000:.0f} ms") # deque-based queue q2 = deque() start = time.perf_counter() for i in range(N): q2.append(i) for _ in range(N): q2.popleft() print(f"deque: {(time.perf_counter() - start) * 1000:.0f} ms")
Expect list to be 50-200× slower. pop(0) is O(n) — shifts everything. Deque is O(1).
Find the shortest path between two nodes in a graph using BFS. Return the path itself, not just the distance.
Hint
from collections import deque def shortest_path(graph, start, end): if start == end: return [start] visited = {start} q = deque([(start, [start])]) while q: node, path = q.popleft() for next_node in graph[node]: if next_node == end: return path + [next_node] if next_node not in visited: visited.add(next_node) q.append((next_node, path + [next_node])) return None graph = { "A": ["B", "C"], "B": ["D"], "C": ["D", "E"], "D": ["F"], "E": ["F"], "F": [], } print(shortest_path(graph, "A", "F")) # ['A', 'B', 'D', 'F']
The path travels along with each queued node. First time we reach the destination, that path is shortest by BFS's nature. Used in GPS navigation, social-network "6 degrees", every shortest-path algorithm.
Mini-Challenge · Restaurant Order Queue
8 minBuild restaurant.py. Simulate a busy kitchen:
- Orders come in (with timestamp) at random.
- The kitchen takes ~3-7 minutes to prepare each.
- Track the average wait time and the longest queue length.
- Simulate 2 hours; print results.
Show one possible solution
# restaurant.py — order queue simulation from collections import deque import random def simulate(minutes=120, order_rate=0.4): queue = deque() times_waited = [] cooking = None # (end_time, order_arrival) max_q = 0 for now in range(minutes): # New order? if random.random() < order_rate: queue.append(now) max_q = max(max_q, len(queue)) # Kitchen finished? if cooking and now >= cooking[0]: wait = cooking[0] - cooking[1] times_waited.append(wait) cooking = None # Start cooking the next order? if cooking is None and queue: order_time = queue.popleft() cook_for = random.randint(3, 7) cooking = (now + cook_for, order_time) print(f"Served : {len(times_waited)}") print(f"Avg wait : {sum(times_waited) / len(times_waited):.1f} min") print(f"Max queue : {max_q}") print(f"Still waiting: {len(queue)}") random.seed(0) simulate()
Non-negotiables: arrivals modelled with a queue, one cooking slot, wait-time tracking. Discrete-event simulation in 30 lines. Real-world adaptation: add more kitchens, priority orders, walk-outs after a wait threshold.
Recap
3 minA queue is FIFO — add at the back, remove from the front. Both operations O(1) when implemented with collections.deque. Don't use a Python list with pop(0) — that's O(n) and turns linear work into quadratic. Queues power schedulers, print spoolers, BFS, simulation. Whenever you see "process in the order arrived", reach for a queue.
Vocabulary Card
- queue
- A FIFO collection. Enqueue (back) + dequeue (front).
- FIFO
- First in, first out.
- deque
- Python's double-ended queue. Both ends O(1). The right backing store.
- BFS
- Breadth-first search. Explores level by level using a queue.
Homework
4 minBuild priority_queue.py. A queue where each item has a priority. dequeue() returns the highest-priority item. Use heapq from the standard library — that's the efficient way.
from heapq import heappush, heappop # heapq is a min-heap. For max behaviour, negate priorities. class PriorityQueue: def __init__(self): self._items = [] self._counter = 0 # tie-breaker — preserves insertion order on ties def enqueue(self, item, priority): ... def dequeue(self): ... # Test: q = PriorityQueue() q.enqueue("low task", priority=5) q.enqueue("URGENT", priority=1) q.enqueue("medium", priority=3) print(q.dequeue()) # URGENT print(q.dequeue()) # medium print(q.dequeue()) # low task
Sample · priority_queue.py
from heapq import heappush, heappop class PriorityQueue: def __init__(self): self._items = [] self._counter = 0 # for tie-breaking def enqueue(self, item, priority): heappush(self._items, (priority, self._counter, item)) self._counter += 1 def dequeue(self): if not self._items: raise IndexError("dequeue from empty PQ") priority, _, item = heappop(self._items) return item def __len__(self): return len(self._items) def is_empty(self): return len(self._items) == 0 q = PriorityQueue() q.enqueue("low task", priority=5) q.enqueue("URGENT", priority=1) q.enqueue("medium", priority=3) q.enqueue("also urgent", priority=1) while not q.is_empty(): print(q.dequeue())
Non-negotiables: heapq backing store, tuple with priority + counter + item. The counter breaks ties — without it, two items with the same priority try to compare each other and may crash. Priority queues power task schedulers, Dijkstra's algorithm, A* pathfinding.