Python Advance 📂 Queue · 1 of 1 70 min read

Queue Types in Python

Master every queue type with visual diagrams. Learn the 5 fundamental patterns — FIFO, LIFO, Priority, Deque, Circular — and every Python implementation: collections.deque, heapq, queue.Queue/LifoQueue/PriorityQueue/SimpleQueue, multiprocessing.Queue and JoinableQueue, asyncio.Queue. Includes a decision flowchart, backpressure diagram, comparison table, and real-world examples for each type.

Section 01

The Story That Explains All Queues

Five Types of Lines You've Stood In
Every day you interact with queues. The coffee shop line (first person in gets served first — FIFO). The stack of trays at a cafeteria (the top one is grabbed first — LIFO). The hospital emergency room (heart attack jumps the queue past a broken finger — priority). The bookstore returns cart (staff can add or take books from either end — deque). The rotating airport luggage belt (fixed loop of positions — circular queue).

Every one of these is a different queue type — and every one has a Python implementation. Choosing the right one is the difference between an elegant program and a slow, buggy one.
🧠
The Core Insight

A queue is not one thing — it's a family of data structures that all share the same idea (add on one side, remove on another) but differ in which side wins. This tutorial covers the 5 fundamental queue types and the 8 Python implementations you'll actually use in production.


Section 02

The Big Picture — All Queue Types At A Glance

📚 Diagram — Five Queue Types Compared Visually
1. FIFO (First In, First Out) Like a coffee shop line IN 4 3 2 1 OUT ← item 1 leaves first 2. LIFO (Last In, First Out) — Stack Like a stack of plates push 4 (top) 3 2 1 pop 4 comes out first 3. Priority Queue Like an ER waiting room IN P1 P2 P3 P5 OUT Auto-sorted by priority Lowest number = wins 4. Deque (Double-Ended) Add/remove from either end L A B C D R L R Both ends are entrances AND exits 5. Circular Queue (Ring Buffer) Fixed size, wraps around 1 2 3 - head & tail pointers move around the loop Never grows in memory

Five fundamental patterns. Every Python queue implementation is one of these five ideas with extra machinery (thread-safety, blocking, size limits, etc.).


Section 03

Type 1 — FIFO Queue (First In, First Out)

The default and most intuitive queue. Items enter at the tail and leave from the head, in the exact order they arrived. This is the "coffee shop line" of data structures.

🍩 Diagram — FIFO Flow With Numbered Items
ENQUEUE (tail) put(5) 5 4 3 2 1 newest (tail) oldest (head) DEQUEUE (head) get() → 1 Item "1" arrived first, so it leaves first. Then "2", then "3"... Order is preserved. This is FAIRNESS.
from collections import deque

# deque = the fast Python FIFO (O(1) at both ends)
q = deque()

q.append("A")       # enqueue at tail
q.append("B")
q.append("C")

print(q.popleft())   # "A" — dequeue from head
print(q.popleft())   # "B"
print(q.popleft())   # "C"
💩
Don't Use A Regular List!

A Python list lets you do lst.pop(0), but it's O(n) — every remaining element has to shift down. For 10,000 items this is 10,000× slower than deque.popleft(). Always use deque or queue.Queue for FIFO.


Section 04

Type 2 — LIFO Queue (Stack)

A stack. Items enter and leave from the same end — the top. The last thing you added is the first thing you get back. Perfect for undo/redo history, function-call frames, and depth-first search.

🍽️ Diagram — LIFO Stack Push & Pop
PUSH (add to top) push("D") D ← TOP C B A ← BOTTOM POP (remove from top) pop() → D C ← TOP B A D was LAST in... D is FIRST out Last In, First Out Only the TOP is accessible — you can't grab item "A" without first popping D, C, B
# Option A: Python list as a stack (works perfectly, O(1) at end)
stack = []
stack.append("A")         # push
stack.append("B")
stack.append("C")
print(stack.pop())     # "C" — last in, first out
print(stack.pop())     # "B"

# Option B: collections.deque (also O(1), thread-safe append/pop)
from collections import deque
stack = deque()
stack.append("A")
stack.append("B")
print(stack.pop())     # "B"

# Option C: queue.LifoQueue (thread-safe, blocking API)
from queue import LifoQueue
s = LifoQueue()
s.put("A"); s.put("B"); s.put("C")
print(s.get())        # "C"
💡
Real Uses of LIFO

Undo/redo history (last action reversed first), function-call stack (last function called returns first), backtracking algorithms, DFS graph traversal, matching brackets/parentheses in a parser, browser back button.


Section 05

Type 3 — Priority Queue

Items don't come out in insertion order. Each item carries a priority, and the item with the highest priority (in Python: the lowest priority number) always comes out first, regardless of when it was added.

🏥 Diagram — Priority Queue Reordering Live
Insert order (whatever, whenever) P5 • "Change ink" P1 • "Heart attack" P3 • "Cough" P1 • "Stroke" P2 • "Broken arm" Min-Heap Structure P1 P1 P2 P3 P5 Get order (by priority) P1 • "Heart attack" P1 • "Stroke" P2 • "Broken arm" P3 • "Cough" P5 • "Change ink" Internally: a min-heap. Insert O(log n), pop-min O(log n).
# Option A: heapq — the low-level, fast min-heap
import heapq

pq = []
heapq.heappush(pq, (5, "Change ink"))
heapq.heappush(pq, (1, "Heart attack"))
heapq.heappush(pq, (3, "Cough"))
heapq.heappush(pq, (1, "Stroke"))
heapq.heappush(pq, (2, "Broken arm"))

while pq:
    print(heapq.heappop(pq))
# (1, 'Heart attack')
# (1, 'Stroke')
# (2, 'Broken arm')
# (3, 'Cough')
# (5, 'Change ink')

# Option B: queue.PriorityQueue — thread-safe, blocking API
from queue import PriorityQueue
pq = PriorityQueue()
pq.put((5, "low urgency"))
pq.put((1, "urgent!"))
print(pq.get())        # (1, 'urgent!')

Section 06

Type 4 — Deque (Double-Ended Queue)

Pronounced "deck". You can add or remove from either end in O(1). A deque can act as a FIFO, a LIFO, or both at once. Python's collections.deque is the most versatile queue in the standard library.

🏗️ Diagram — Deque: All Four Operations
A B C D E LEFT end RIGHT end appendleft(X) insert at left popleft() → A remove from left append(Y) insert at right pop() → E remove from right Four operations, all O(1). Use as FIFO (append + popleft) or LIFO (append + pop) or both.
from collections import deque

d = deque(["B", "C", "D"])

# Add to either end
d.appendleft("A")      # [A, B, C, D]
d.append("E")           # [A, B, C, D, E]

# Remove from either end
print(d.popleft())     # "A"  ← [B, C, D, E]
print(d.pop())         # "E"  ← [B, C, D]

# Bounded deque — auto-drops from the OPPOSITE end when full
recent = deque(maxlen=3)
for i in range(6):
    recent.append(i)
print(recent)              # deque([3, 4, 5], maxlen=3)

# Rotate — very fast circular shift
d = deque([1, 2, 3, 4, 5])
d.rotate(2)               # deque([4, 5, 1, 2, 3])
d.rotate(-1)              # deque([5, 1, 2, 3, 4])
🏆
Killer Feature — maxlen

deque(maxlen=100) gives you a bounded queue for free — perfect for "last N events" ring buffers (recent log lines, moving average window, LRU-style caches). New items push old ones out automatically.


Section 07

Type 5 — Circular Queue (Ring Buffer)

A fixed-size queue where the head and tail pointers wrap around the end of the storage array back to the start. It never grows, never shrinks — perfect for fixed-memory environments (embedded systems, audio buffers, network packet rings).

🔁 Diagram — Circular Queue With Wrap-Around
A [0] B [1] C [2] D [3] tail - [4] - [5] - [6] - [7] HEAD (next to remove) TAIL (next to insert) flow ↻ Fixed-size buffer, e.g. 8 slots. When TAIL reaches the end, it wraps back to [0]. If full: reject, block, or overwrite.

Perfect for streaming: audio, video frames, sensor readings, network packets. Memory usage is constant regardless of throughput.

Implementing a Circular Queue From Scratch

class CircularQueue:
    def __init__(self, capacity):
        self.buf   = [None] * capacity
        self.cap   = capacity
        self.head  = 0       # index to read from
        self.tail  = 0       # index to write to
        self.size  = 0

    def enqueue(self, item):
        if self.size == self.cap:
            raise OverflowError("Queue full")
        self.buf[self.tail] = item
        self.tail = (self.tail + 1) % self.cap    # wrap around!
        self.size += 1

    def dequeue(self):
        if self.size == 0:
            raise IndexError("Queue empty")
        item = self.buf[self.head]
        self.buf[self.head] = None
        self.head = (self.head + 1) % self.cap    # wrap around!
        self.size -= 1
        return item

    def __len__(self): return self.size


# Demo
cq = CircularQueue(4)
for ch in "ABCD":
    cq.enqueue(ch)
print(cq.dequeue(), cq.dequeue())    # A B
cq.enqueue("E"); cq.enqueue("F")         # tail wraps back to slot 0
print(cq.buf)                            # ['E', 'F', 'C', 'D']

Shortcut — Use deque(maxlen=…) As A Circular Buffer

from collections import deque

# A ring of the last 5 sensor readings
ring = deque(maxlen=5)
for reading in [10, 11, 13, 12, 14, 15, 16]:
    ring.append(reading)     # old readings drop off the LEFT automatically

print(ring)                # deque([13, 12, 14, 15, 16], maxlen=5)
print(sum(ring) / len(ring))  # rolling average

Section 08

Python's Queue Module — Thread-Safe Implementations

The four types above are conceptual. When you need thread-safe versions with blocking put/get and size limits, use the queue module.

📚 Diagram — The queue Module Family
queue module Queue FIFO (default) LifoQueue LIFO (stack) PriorityQueue min-heap (priority) SimpleQueue FIFO, unbounded (fastest) All four share the same API put() • get() • qsize() • empty() • full() put_nowait() • get_nowait() • task_done() • join() (SimpleQueue lacks join / task_done)
ClassBehaviourBounded?join/task_done?When to use
Queue FIFO Yes (maxsize) Yes Default work queue for threads
LifoQueue LIFO (stack) Yes Yes DFS, undo history across threads
PriorityQueue Priority (min-heap) Yes Yes Task scheduling with urgency
SimpleQueue FIFO No (unbounded) No Fastest option for simple hand-offs
from queue import Queue, LifoQueue, PriorityQueue, SimpleQueue

# 1. Standard FIFO Queue
q = Queue(maxsize=10)
q.put("A"); q.put("B"); q.put("C")
print(q.get())          # A

# 2. Stack (LIFO)
s = LifoQueue()
s.put("A"); s.put("B"); s.put("C")
print(s.get())          # C — last in, first out

# 3. Priority (lowest number wins)
p = PriorityQueue()
p.put((3, "low"))
p.put((1, "urgent"))
p.put((2, "med"))
print(p.get())          # (1, 'urgent')

# 4. SimpleQueue — no maxsize, no join(), fastest
sq = SimpleQueue()
sq.put("fast")
print(sq.get())         # fast

Section 09

Multiprocessing Queues — Cross-Process Communication

The queue module only works between threads (same memory space). For processes, use multiprocessing.Queue or JoinableQueue — they pickle and pipe objects across process boundaries.

📫 Diagram — Threading Queue vs Multiprocessing Queue
queue.Queue (threads) Single Python Process Thread A Thread B Queue in-memory list Zero pickle overhead objects passed by reference multiprocessing.Queue Process A q.put(obj) Process B q.get() OS Pipe + Serialised Bytes pickle → send → unpickle Pickle overhead per item but works across processes
from multiprocessing import Process, Queue, JoinableQueue

def worker(q):
    while True:
        item = q.get()
        if item is None: break
        print(f"got {item}")
        q.task_done()             # only on JoinableQueue

if __name__ == "__main__":
    q = JoinableQueue()          # like Queue + task_done/join

    p = Process(target=worker, args=(q,))
    p.start()

    for i in range(5):
        q.put(f"task {i}")

    q.join()                     # wait for all task_done()
    q.put(None)                  # poison pill
    p.join()

Section 10

Asyncio Queue — For Async/Await Code

When you're inside asyncio code, you want asyncio.Queue. It has the same API as queue.Queue, but its put()/get() return coroutines you must await. It's NOT thread-safe — it's coroutine-safe within a single event loop.

import asyncio

async def producer(q, name):
    for i in range(5):
        await asyncio.sleep(0.1)
        await q.put(f"{name}-{i}")

async def consumer(q, name):
    while True:
        item = await q.get()
        if item is None: break
        print(f"[{name}] got {item}")
        q.task_done()

async def main():
    q = asyncio.Queue(maxsize=10)

    prods = [asyncio.create_task(producer(q, f"P{i}")) for i in range(2)]
    cons  = [asyncio.create_task(consumer(q, f"C{i}")) for i in range(3)]

    await asyncio.gather(*prods)
    await q.join()

    for _ in cons:
        await q.put(None)
    await asyncio.gather(*cons)

asyncio.run(main())
⚠️
Never Mix Queue Types Across Contexts

queue.Queue in async code will block the event loop.
asyncio.Queue from threads will not synchronise correctly.
multiprocessing.Queue in threads is legal but overkill.
One rule: pick the queue that matches your concurrency model.


Section 11

heapq — The Raw Priority Queue Building Block

heapq gives you a min-heap on a regular Python list. It's what PriorityQueue uses internally, but without any locking overhead. Fastest choice for single-threaded priority work.

import heapq

# A heap is just a plain list, treated with heap operations
h = []
heapq.heappush(h, 3)
heapq.heappush(h, 1)
heapq.heappush(h, 4)
heapq.heappush(h, 1)
heapq.heappush(h, 5)

print(heapq.heappop(h))       # 1 (smallest first)
print(heapq.heappop(h))       # 1
print(heapq.heappop(h))       # 3

# Turn an existing list into a heap in O(n)
data = [7, 3, 9, 1, 4, 2]
heapq.heapify(data)             # in-place: [1, 3, 2, 7, 4, 9]
print(data)

# Top-K largest / smallest in O(n log k)
print(heapq.nlargest(3, data))   # [9, 7, 4]
print(heapq.nsmallest(3, data))  # [1, 2, 3]

# Priority queue with tie-breaker (avoids TypeError on equal priorities)
import itertools
counter = itertools.count()
pq = []
heapq.heappush(pq, (1, next(counter), "first urgent"))
heapq.heappush(pq, (1, next(counter), "second urgent"))
heapq.heappush(pq, (3, next(counter), "normal"))
while pq:
    print(heapq.heappop(pq)[2])

Section 12

Bounded vs Unbounded Queues — The Backpressure Diagram

🏇 Diagram — What Happens When The Queue Fills Up
UNBOUNDED — Queue() Grows forever Fast Prod grows...grows...grows Slow Cons RAM usage → OOM crash producer wins the race, memory dies BOUNDED — Queue(maxsize=5) Producer BLOCKS when full Fast Prod (blocks) FULL (5/5) Cons RAM stays flat — safe producer waits for consumer — "backpressure"

A maxsize parameter converts the queue into a natural rate limiter. When full, producers automatically slow down to match consumer speed.


Section 13

Real-World Scenarios — Which Queue For What

📨
Web Crawler
URLs to visit form a FIFO queue. Multiple worker threads pull URLs and push newly-discovered links. Use queue.Queue with maxsize to prevent memory blow-up.
queue.Queue (FIFO)
🛠️
Undo/Redo History
Each user action pushed onto a stack; undo pops the last one. Redo needs a second stack. Use collections.deque with maxlen for bounded history.
deque as stack (LIFO)
🚨
Alert Router
P1 (critical) alerts must jump ahead of P4 (info). Use PriorityQueue so on-call engineers see the worst issues first, regardless of arrival time.
PriorityQueue
🎧
Audio Streaming Buffer
Fixed number of audio frames in memory. New frames overwrite oldest when the consumer falls behind. Perfect for a circular buffer via deque(maxlen=N).
deque(maxlen=...) circular
🖥️
CPU-Heavy Batch Job
Distribute tasks across processes. Use multiprocessing.Queue or multiprocessing.JoinableQueue for coordination with task_done()/join().
multiprocessing.Queue
🌐
Async HTTP Server
Coroutines producing/consuming messages without blocking the event loop. Use asyncio.Queue — never queue.Queue, which would freeze the loop.
asyncio.Queue

Section 14

Practical Example — Multi-Queue Task Scheduler

A worker that pulls from THREE queues in strict priority order: emergency tasks first, then normal tasks, then background maintenance.

from queue import Queue
import threading, time, random

emergency  = Queue()
normal     = Queue()
background = Queue()

def worker():
    while True:
        # Try each queue in priority order
        for name, q in [("emergency", emergency),
                        ("normal",    normal),
                        ("bg",        background)]:
            try:
                task = q.get_nowait()
                if task is None: return
                print(f"[{name}] processing {task}")
                time.sleep(0.1)
                break
            except:
                continue
        else:
            time.sleep(0.05)     # all empty, wait a bit

# Kick off a few workers
threads = [threading.Thread(target=worker) for _ in range(3)]
for t in threads: t.start()

# Feed mixed tasks
for i in range(10): background.put(f"bg-{i}")
for i in range(5):  normal.put(f"task-{i}")
for i in range(2):  emergency.put(f"fire-{i}")

time.sleep(3)
for _ in threads: emergency.put(None)
for t in threads: t.join()
OUTPUT (order shows priority routing)
[emergency] processing fire-0 [emergency] processing fire-1 [normal] processing task-0 [normal] processing task-1 [normal] processing task-2 ... [bg] processing bg-0 [bg] processing bg-1 ...

Section 15

Complete Comparison Table — All Queue Types

Implementation Type Thread-safe Process-safe Blocking API Speed
list (pop/append) LIFO stack only No* No No Fastest
collections.deque FIFO / LIFO / Deque append/pop only No No Very fast
heapq Priority (min-heap) No No No Very fast
queue.Queue FIFO Yes No Yes Medium
queue.LifoQueue LIFO Yes No Yes Medium
queue.PriorityQueue Priority Yes No Yes Medium
queue.SimpleQueue FIFO (unbounded) Yes No Yes Fast
multiprocessing.Queue FIFO Yes Yes Yes Slow (pickle)
multiprocessing.JoinableQueue FIFO + task_done Yes Yes Yes Slow (pickle)
asyncio.Queue FIFO No No Yes (await) Fast (in-loop)
asyncio.LifoQueue LIFO (async) No No Yes Fast
asyncio.PriorityQueue Priority (async) No No Yes Fast

*A Python list is atomic for single append/pop from the same end, but mixed operations from multiple threads can still race.


Section 16

Decision Flowchart — Which Queue Should I Use?

🧠 Diagram — Pick The Right Queue In 3 Questions
Q1: Concurrency model? single / threads / processes / async single-thread threads processes asyncio Q2: which type? FIFO/LIFO/prio? FIFO/LIFO → deque Priority → heapq Q2: which type? FIFO/LIFO/prio? FIFO → queue.Queue LIFO → LifoQueue Priority → PriorityQueue Q3: need join? task_done/join? Yes → mp.JoinableQueue No → mp.Queue large data? use shared_memory Q2: which type? FIFO/LIFO/prio? FIFO → asyncio.Queue LIFO → asyncio.LifoQueue Priority → asyncio.PriorityQueue SPECIAL CASES Fixed-size ring / "last N items" → deque(maxlen=N) Top-K largest / smallest → heapq.nlargest / nsmallest 3 questions: how many concurrent things, which queue type, do I need task_done?

Section 17

Golden Rules

🔑 Queue Types — Non-Negotiable Rules
1
Never use a plain list as a FIFO. list.pop(0) is O(n) and quietly kills performance. Use collections.deque or queue.Queue.
2
A Python list is fine as a stackappend and pop from the end are both O(1). If you need thread safety, use queue.LifoQueue.
3
Match the queue to the concurrency model: threads → queue module, processes → multiprocessing, asyncio → asyncio.Queue. Never cross the streams.
4
Always set maxsize on long-running queues. Unbounded queues become memory bombs the moment producers outrun consumers.
5
For priority queues with equal priorities, include a tie-breaker counter or use @dataclass(order=True) with compare=False on the data field. Otherwise Python tries to compare your objects and raises TypeError.
6
Use deque(maxlen=N) for any "last N items" pattern — recent log lines, rolling average, LRU-ish ring. It's simpler than a full circular queue class.
7
Prefer queue.SimpleQueue for simple thread hand-offs. It's faster than Queue because it lacks the task_done/join machinery you rarely need.
8
For multiprocessing.Queue, keep messages small. Every put/get is a pickle round-trip. For big NumPy arrays, use shared_memory and pass only the shared-memory name across the queue.
9
Every get() must be paired with a task_done() in a finally block if you use join(). Otherwise join() hangs forever on the first exception.
10
Shut consumers down with poison pills — one None per worker after your real work is done. Cleaner than killing threads or processes.
11
For single-threaded priority work, use heapq directly. It's several times faster than PriorityQueue because it skips all the locking.
You have completed Queue. View all sections →