The Story That Explains Queues
That is a Queue: First In, First Out (FIFO). The front of the line is called the head (next to be served) and the back is called the tail (where new arrivals join).
Now imagine a VIP lane: guests with gold-tier passes skip ahead. Priority matters, not arrival order — that is a Priority Queue. Finally, imagine a flexible rope that lets people join or leave from either end — that is a Deque (double-ended queue).
All three structures share one DNA: they manage ordered access to elements. The difference is which end you can touch and which element comes out next.
Queues power CPU task scheduling, printer spoolers, BFS graph traversal, network packet routing, event loops in JavaScript/Node, and async job pipelines. Knowing when to reach for a regular Queue vs Deque vs Priority Queue separates junior from senior engineers.
Queue Anatomy — FIFO Visual
Python Code — Regular Queue with collections.deque
Python has no dedicated Queue class for general algorithmic use.
The correct tool is collections.deque, which gives O(1) append and popleft —
unlike a plain list whose pop(0) is O(n).
my_list.pop(0) is O(n) — every element shifts left in memory. On 1 million items this is
catastrophically slow. Always use collections.deque for queue operations.
from collections import deque
# ── Create an empty queue ──────────────────────────────────
q = deque()
# ── Enqueue (add to tail) ──────────────────────────────────
q.append("ticket_001") # O(1)
q.append("ticket_002")
q.append("ticket_003")
print("Queue:", q)
# ── Peek at head (without removing) ───────────────────────
print("Head:", q[0]) # O(1)
# ── Dequeue (remove from head) ────────────────────────────
served = q.popleft() # O(1)
print("Served:", served)
print("Remaining:", q)
# ── Size check ────────────────────────────────────────────
print("Size:", len(q))
print("Empty?", len(q) == 0)
# ── Drain the queue ───────────────────────────────────────
while q:
print("→ Serving", q.popleft())
collections.deque is implemented as a doubly-linked list of fixed-size blocks.
Both append() (right) and popleft() (left) are guaranteed O(1) amortised.
For thread-safe queuing between producer/consumer threads, use queue.Queue instead.
Deque — Double-Ended Queue
Python Deque — Full API
from collections import deque
dq = deque([10, 20, 30])
# ── Add elements ──────────────────────────────────────────
dq.append(40) # right → [10,20,30,40] O(1)
dq.appendleft(5) # left → [5,10,20,30,40] O(1)
# ── Remove elements ───────────────────────────────────────
r = dq.pop() # right → 40 O(1)
l = dq.popleft() # left → 5 O(1)
print(dq) # deque([10, 20, 30])
# ── Peek ──────────────────────────────────────────────────
print(dq[0], dq[-1]) # 10 30 — O(1) both ends
# ── Rotate (useful for round-robin scheduling) ────────────
dq.rotate(1) # [30, 10, 20] right-rotate by 1
dq.rotate(-1) # [10, 20, 30] back to original
# ── Bounded deque (sliding window) ───────────────────────
window = deque(maxlen=3)
for x in [1, 2, 3, 4, 5]:
window.append(x)
print(window) # deque([3, 4, 5], maxlen=3)
deque(maxlen=k) automatically evicts the oldest element when a new one is added beyond capacity.
This is the cleanest way to implement a sliding window in Python — used in
moving averages, rate limiters, and stream processing. Zero extra code needed.
Priority Queue — Heap-Based Ordering
A Priority Queue serves elements in priority order, not arrival order.
Python's heapq module implements a min-heap: the smallest element always exits first.
For a max-heap, negate your values.
Python heapq — Complete Priority Queue
import heapq
# ── Min-Heap (default) ────────────────────────────────────
heap = []
heapq.heappush(heap, (3, "Task C"))
heapq.heappush(heap, (1, "Task A")) # priority 1 → highest
heapq.heappush(heap, (2, "Task B"))
heapq.heappush(heap, (5, "Task E"))
# Peek at root (min element) — O(1)
print("Next:", heap[0])
# Pop smallest — O(log n)
while heap:
prio, task = heapq.heappop(heap)
print(f"Serving [{prio}] {task}")
# ── Max-Heap trick: negate priority ──────────────────────
max_heap = []
for prio, task in [(3,"C"),(1,"A"),(5,"E")]:
heapq.heappush(max_heap, (-prio, task)) # negate!
neg_prio, task = heapq.heappop(max_heap)
print(f"Max-heap top: [{-neg_prio}] {task}")
# ── heapify an existing list — O(n) ──────────────────────
data = [(4,"D"), (2,"B"), (7,"G"), (1,"A")]
heapq.heapify(data)
print("Heapified root:", data[0])
If two tasks share the same priority, Python will try to compare the task objects directly — which crashes if they're not comparable.
Use a 3-tuple (priority, counter, item) where counter is an ever-increasing integer
from itertools.count(). The counter acts as a stable tie-breaker with zero extra logic.
Time & Space Complexity Table
| Operation | Queue (deque) | Deque (both ends) | Priority Queue (heapq) | list as queue |
|---|---|---|---|---|
| Enqueue / Push | O(1) | O(1) each end | O(log n) | O(1) amort. |
| Dequeue / Pop front | O(1) | O(1) | O(log n) | O(n) ⚠️ |
| Peek / Min | O(1) | O(1) each end | O(1) | O(1) |
| Search by value | O(n) | O(n) | O(n) | O(n) |
| heapify existing list | N/A | N/A | O(n) | N/A |
| Space | O(n) | O(n) | O(n) | O(n) |
| Thread-safe? | No | No | No | No |
None of the structures above are thread-safe. For producer-consumer patterns across threads,
use queue.Queue (FIFO), queue.LifoQueue (stack), or
queue.PriorityQueue — all from Python's standard queue module,
with built-in locking.
Real-World Use Cases
queue.Queue so multiple threads can submit
jobs safely without race conditions.
heapq models this exactly: push (severity, patient),
pop always returns the most urgent case.
maxlen=N).
Undo pops from the right; redo re-applies from a second deque.
The bounded size auto-discards ancient history.
Real-World Pattern — BFS with Queue
from collections import deque
def bfs(graph, start):
"""Breadth-First Search — returns visit order."""
visited = set()
queue = deque([start])
order = []
while queue:
node = queue.popleft() # O(1) — always deque, never list
if node in visited:
continue
visited.add(node)
order.append(node)
for neighbour in graph.get(node, []):
if neighbour not in visited:
queue.append(neighbour)
return order
# Example graph (adjacency list)
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [], 'E': [], 'F': []
}
print(bfs(graph, 'A'))
Because BFS explores all nodes at depth d before depth d+1, the first time you reach a target node, the path taken is guaranteed to be the shortest in terms of edge count. This is the foundation of shortest-path algorithms in unweighted graphs, maze solvers, and social distance calculators.
Real-World Pattern — Dijkstra with Priority Queue
import heapq
def dijkstra(graph, start):
"""Shortest weighted path — Priority Queue drives node selection."""
dist = {node: float('inf') for node in graph}
dist[start] = 0
pq = [(0, start)] # (cost, node)
while pq:
cost, node = heapq.heappop(pq) # always picks cheapest node
if cost > dist[node]:
continue # stale entry — skip
for nbr, weight in graph[node]:
new_cost = cost + weight
if new_cost < dist[nbr]:
dist[nbr] = new_cost
heapq.heappush(pq, (new_cost, nbr))
return dist
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('C', 2), ('D', 5)],
'C': [('D', 1)],
'D': []
}
print(dijkstra(graph, 'A'))
At each iteration, Dijkstra must pick the unvisited node with the minimum accumulated cost. A min-heap does this in O(log n). Without it, you'd scan all nodes in O(n) per step, blowing total complexity from O((V+E) log V) to O(V²) — catastrophic on large sparse graphs.
Queue vs Deque vs Priority Queue — At a Glance
Queue Operations — Pipeline View
deque(), [] for heapq, or deque(iterable, maxlen=k) for bounded.append() for right end, appendleft() for left end, heappush() for priority. All O(1) or O(log n).dq[0] (head), dq[-1] (tail), heap[0] (min). All O(1).popleft(), pop(), heappop(). Core operation — triggers business logic on the extracted element.while queue: or if not queue: raise before popping. Popping an empty queue raises IndexError or pops returns undefined.Golden Rules
list.pop(0) as a queue.
It is O(n). Import collections.deque and call popleft().
The difference on 100,000 items: microseconds vs tens of milliseconds per operation.
(priority, counter, item) in heapq
whenever items might share the same priority. A unique counter from
itertools.count() prevents Python from attempting to compare
incomparable objects and crashing with a TypeError.
heapq is a min-heap only. Push (-priority, item)
and negate when popping. Alternatively, wrap values in a class with
__lt__ reversed — but negation is simpler and idiomatic.
queue.Queue, not deque.
collections.deque has no internal lock. In multi-threaded producers/consumers,
use queue.Queue (FIFO) or queue.PriorityQueue which wrap
all mutations in a threading.Lock.
deque(maxlen=k) for sliding windows, not manual trimming.
The bounded deque auto-evicts the oldest element when full — clean, O(1), and zero
off-by-one errors. It replaces the classic append-then-slice anti-pattern.
while queue:.
Popping from an empty deque raises IndexError;
heappop from an empty list raises IndexError too.
Defensive checks prevent silent crashes in production pipelines and BFS loops.