Data Structure 📂 Data Structures (Core) · 4 of 6 34 min read

Queues, Deques & Priority Queues in Python

A fully animated, use-case-driven tutorial covering all three queue variants in Python. Starts with a cinema seats analogy, walks through interactive step-by-step animations for Queue (FIFO), Deque (double-ended), and Priority Queue (heapq min-heap).

Section 01

The Story That Explains Queues

Cinema Seats & the Ticket Queue
Picture the queue outside a cinema on premiere night. The person who arrived first gets their ticket first and walks through the door first. Nobody cuts in line. Nobody at the back gets served ahead of someone who arrived earlier.

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.

🎯
Why Queues Matter

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.


Section 02

Queue Anatomy — FIFO Visual

🎬 Queue Vocabulary
enqueue
Add an element at the tail (back). Like joining the cinema line.
dequeue
Remove the element at the head (front). The first person enters the cinema.
peek
Look at the head element without removing it. See who's next without calling them.
empty?
Check if the queue has zero elements. The cinema is empty — doors close.
🎞️ Queue Animation — click to step through
▶ Press Next to begin

Section 03

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).

⚠️
Never Use list.pop(0) as a Queue

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())
OUTPUT
Queue: deque(['ticket_001', 'ticket_002', 'ticket_003']) Head: ticket_001 Served: ticket_001 Remaining: deque(['ticket_002', 'ticket_003']) Size: 2 Empty? False → Serving ticket_002 → Serving ticket_003
deque Is the Pythonic Queue

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.


Section 04

Deque — Double-Ended Queue

The Revolving Stage Door
A normal cinema queue only lets people join at the back and exit at the front. Now imagine a special stage door that can be opened from both sides — actors can enter or exit from either the front or the rear. That flexibility is a Deque: you can enqueue and dequeue from either end in O(1).
🎭 Deque Animation — click to step through
◀ appendleft / popleftappend / pop ▶
▶ Press Next to begin

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)
OUTPUT
deque([10, 20, 30]) 10 30 deque([3, 4, 5], maxlen=3)
💡
maxlen — The Sliding Window Superpower

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.


Section 05

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.

🏅 Priority Queue Animation — lower number = higher priority
Priority → bar height (lower = served first)
▶ Press Next to begin

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])
OUTPUT
Next: (1, 'Task A') Serving [1] Task A Serving [2] Task B Serving [3] Task C Serving [5] Task E Max-heap top: [5] E Heapified root: (1, 'A')
🔑
tie-breaking with (priority, counter, item)

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.


Section 06

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
🧵
Thread Safety

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.


Section 07

Real-World Use Cases

🌐
BFS Graph Traversal
Queue (FIFO)
Breadth-First Search visits nodes level by level. A deque acting as a FIFO queue ensures you explore all neighbours at distance k before distance k+1. Used in shortest path, social network distance, and web crawlers.
🖨️
Print Spooler
Queue (FIFO)
Print jobs enter a queue and are served in order. No job jumps the line. The OS wraps this in queue.Queue so multiple threads can submit jobs safely without race conditions.
🏥
Hospital Triage
Priority Queue
Patients with critical conditions (priority 1) are seen before stable ones (priority 5). heapq models this exactly: push (severity, patient), pop always returns the most urgent case.
📊
Sliding Window Max
Deque (monotonic)
A deque stores indices of candidates for the window maximum. Elements are removed from the right when they're no longer the maximum, and from the left when they leave the window. O(n) total.
⚙️
CPU Task Scheduler
Priority Queue
OS schedulers assign processes a priority. A min-heap (inverted for max-priority) always yields the highest-priority runnable process in O(log n) amid thousands of queued tasks.
🔄
Undo / Redo Buffer
Deque (bounded)
Text editors store the last N actions in a bounded deque (maxlen=N). Undo pops from the right; redo re-applies from a second deque. The bounded size auto-discards ancient history.

Section 08

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'))
OUTPUT
['A', 'B', 'C', 'D', 'E', 'F']
🌲
BFS Guarantees Shortest Path (Unweighted)

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.


Section 09

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'))
OUTPUT
{'A': 0, 'B': 1, 'C': 3, 'D': 4}
🗺️
Why the Priority Queue Is the Heart of Dijkstra

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.


Section 10

Queue vs Deque vs Priority Queue — At a Glance

🎟️
Queue (FIFO)
Strict first-in first-out. Only touch the front and back. Use for: BFS, task scheduling, print spoolers, event loops.
deque.append() + deque.popleft()
↔️
Deque (Double-Ended)
Add/remove from either end in O(1). Use for: sliding windows, undo-redo, palindrome checks, monotonic window problems.
appendleft / popleft / append / pop
🏅
Priority Queue
Always serves the minimum (or maximum) element first. Use for: Dijkstra, A*, Huffman coding, task scheduling by urgency, k-smallest/largest.
heapq.heappush / heappop

Section 11

Queue Operations — Pipeline View

01
Create
Initialise the data structure — deque(), [] for heapq, or deque(iterable, maxlen=k) for bounded.
02
Produce (Enqueue)
Add elements: append() for right end, appendleft() for left end, heappush() for priority. All O(1) or O(log n).
03
Inspect (Peek)
View without removing: dq[0] (head), dq[-1] (tail), heap[0] (min). All O(1).
04
Consume (Dequeue)
Remove and return: popleft(), pop(), heappop(). Core operation — triggers business logic on the extracted element.
05
Guard (Empty Check)
Always check while queue: or if not queue: raise before popping. Popping an empty queue raises IndexError or pops returns undefined.

Section 12

Golden Rules

🎯 Queues, Deques & Priority Queues — 6 Non-Negotiable Rules
1
Never use 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.
2
Use a 3-tuple (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.
3
For max-priority, negate the priority value. Python's 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.
4
For thread safety, use 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.
5
Use 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.
6
Guard every pop with an empty check or 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.