◈ python mapVol 4 · Ch 52/62
Volume 4 Python, from the metal up · chapter 52

52The queue — first in, first out

In Chapter 51 we built the stack — last in, first out. It always hands you back the newest thing you gave it. Here we build its mirror image, and it turns out to be everywhere. A queue is how your operating system shares the CPU. It is how a search sweeps a maze level by level, and how every keystroke you type waits its turn. The rule is dead simple: First In, First Out. Add at the back, remove from the front, and nobody jumps the line. Here's the plan. We'll write the obvious Python queue out of a plain list, then watch it quietly betray us on big inputs. After that we fix it two ways. First a ring buffer, which bends a fixed array into a circle. Then collections.deque, the tool you'll actually reach for. And the whole way through we keep asking the one question that decides everything: when you pull the item off the front, what does the machine actually have to move? By the end you'll have a queue that adds and removes in true O(1). You'll know exactly why the natural version secretly crawls at O(n). And you'll have seen — in memory — the one trick that fixes it.

★ YOU ALREADY RUN THIS · the-till-linenobody ever taught you the rule — but you would riot if it broke
Friday evening, one till open, and you push your trolley in behind the man with the crate of beer. You do not pick a spot: you join at the back, because the back is the only door in. Then somebody drifts up beside the belt with two items and starts easing in ahead of you, and something in your chest goes tight. Nobody taught you that rule. You felt it break. And look at who is being served — the woman at the front is there for exactly one reason: she has been standing there longest. Everyone behind her holds position. Nobody shuffles up. The line moves by one person leaving the front and one joining the back, and nothing else in it ever moves.
you join at the back — the only door inq.append(x) · enqueue, and the tail is the only place it happens
the till serves whoever stands at the frontq.popleft() · dequeue — always the oldest survivor
the queue-jumper makes your chest go tightFIFO is an invariant, not a preference — break it and it stopped being a queue
nobody shuffles up when the front leavesthe items never move — only the two ends do. That is the whole O(1)
the cafeteria tray stack hands you the newest tray (ch 51)same two moves, opposite end — a stack serves the newest, a queue the oldest
pin it: the front of a queue is never a decision — it is simply whoever has waited longest, and every line of this chapter exists to keep that true.
iolinked · chapter 52 — the checkpoints6 steps
$ sections covered in The queue — first in, first out
01One rule, and it means order
02Why a plain list makes a terrible queue
03The ring buffer — two indices chasing around a fixed array
04collections.deque — the real fix, and its memory
05How the machine feels it, and why search runs on queues
06When to reach for a queue

01One rule, and it means order

Let's start with the shape of the thing. A queue is a linear collection with two doors, one at each end, and a law about which door does what. The back door is enqueue: a new item goes in there, and only there. The front door is dequeue: an item comes out there, and only there. Watch what that one law forces — because you always remove the oldest surviving item, the one that has waited longest, the queue preserves arrival order perfectly. First in, first out. A stack is LIFO; a queue is FIFO; both are nothing more than a rule about which end you touch.

Let's make that concrete before we go further. Say three web requests hit your server in order: R1, then R2, then R3. Each one enqueues at the back as it arrives, so the line reads R1, R2, R3 from front to back. Now the server is ready to work. It dequeues from the front and gets R1, the one that has waited longest. Dequeue again and it gets R2, then R3. The order out is exactly the order in. Nobody who arrived later was served first. That is the whole promise of a queue, and every use in this chapter is just this small trace played out at scale.

And that rule is not a toy. FIFO is fairness: the request that arrived first gets served first, so no task starves while newer ones cut the line. It is also buffering, and the picture there is a fast producer feeding a slow consumer — your keyboard feeding a busy program, or a network card feeding your app. A queue holds that backlog in the exact order it arrived, and the consumer drains it at its own pace without losing or reordering a thing. Keep those two words, fairness and buffering, in mind, because every real use at the end of this chapter is one of them.

⚠ MOST BEGINNERS THINK…“if I took from the wrong end, I would know”
This one is not naive — it is an inference from something the last chapter proved true. A queue is not an object you import; it is an ordinary container held to one promise: add at one end, take from the other. Good. So jobs = [], then jobs.append(task) when work lands and jobs.pop() when a worker frees up, and the promise is kept by hand. Kept by hand is fine, the reasoning goes, because a broken promise announces itself: fumble the end and the pipeline comes out backwards, the output is visibly scrambled, the test goes red, and it is fixed long before a customer meets it. A wrong end is a loud bug.
TYPE THIS — 10 SECONDS
from collections import deque

jobs, real = [], deque()

for t in ("t1", "t2", "t3", "t4"):     # a trickle: each job served the moment it lands
    jobs.append(t); real.append(t)
    print(jobs.pop(), real.popleft(), end="   ")
print("<- identical")

for t in ("t1", "t2", "t3", "t4"):     # a burst: four land before anyone is served
    jobs.append(t); real.append(t)
print("backlog, list :", [jobs.pop() for _ in range(4)])
print("backlog, deque:", [real.popleft() for _ in range(4)])
t1 t1   t2 t2   t3 t3   t4 t4   <- identical
backlog, list : ['t4', 't3', 't2', 't1']
backlog, deque: ['t1', 't2', 't3', 't4']
Both containers hand back the identical sequence for as long as the consumer keeps up, so the wrong end stays invisible until a backlog forms — and a backlog is the one condition your laptop never reproduces and production reaches by lunchtime. Be exact about the boundary rather than hand-wavy: the two agree on every removal that finds one item waiting, and diverge on every removal that finds two or more. And what the burst above exposes as merely backwards gets quieter, not louder, under real load. Run the same pipeline for a thousand ticks with two jobs arriving per tick and one served — a modest, ordinary overload — and the list version reports a worst wait of 0 ticks and a mean of 0.0. A flawless dashboard, because it only ever serves the job that just landed; job number zero is still sitting in that list after a thousand ticks, and no metric built from served jobs can see the ones that never were. The deque version reports a worst wait of 500 ticks and a mean of 250.0, which reads far worse and is simply the truth. That is the real cost of leaving FIFO in your fingers: nothing is watching them. Not the type — it is a list either way. Not the test — it passes. And least of all the dashboard, which gets prettier as the starvation gets worse. So the guard cannot be care and it cannot be a graph; it has to be the call itself, and popleft is the call that says out loud which door it opened. Note too that this is only the order half of the bill for a list-shaped queue; section 02 next door prices the other half.

Now the part this volume cares about most: what does a queue actually look like in RAM? The honest first picture is a row of slots — a contiguous block, exactly the array of 8-byte references from Volume 1, plus two markers. One marker, front, points at the oldest live item, the next one to leave. The other, back, points at the next free slot, where the next arrival will land. To enqueue, we write at back and nudge back forward. To dequeue, we read at front and nudge front forward. Nothing else moves.

Put real addresses on that picture and it stops being abstract. Say the block starts at address 1000 and each reference is 8 bytes, so slot 0 lives at 1000, slot 1 at 1008, slot 2 at 1016, and on up. If front holds the index 2 and back holds 5, the live items sit in slots 2, 3, and 4 — three of them — at addresses 1016, 1024, and 1032. To dequeue, we read slot 2 and bump front to 3. The value at 1016 never budged — we just stopped pointing at it. That is the whole trick in one line: the data stays put and two small integers do all the walking.

a queue in memory — a contiguous array of 8-byte references (Vol 1) 0x1a00 0x1a08 0x1a10 0x1a18 0x1a20 0x1a28 0x1a30 0x1a38 · · A B C D · · 01 23 45 67 front dequeue reads here (A leaves first) back enqueue writes here, then back → 7
The queue, from memory up — four live items (A oldest, D newest) sit in a contiguous run of reference slots. Two integers do all the work: front marks the next to leave, back the next free spot. Enqueue and dequeue each touch one slot and move one marker.

If both operations touch a single slot and bump a single index, both should be O(1). So why does the most natural Python queue crawl to a halt on big inputs? →

02Why a plain list makes a terrible queue

Reach for a queue in Python and the obvious move is a list: append to add at the back, pop(0) to remove from the front. It looks right. It even works — for a while. Then it dies, and the reason is buried in how a list lives in memory. Recall from Volume 1: a Python list is a contiguous array of references with a fixed front pinned at index 0. There is no movable front marker. So when you delete index 0, the list cannot just "advance a pointer" — it must keep index 0 meaning index 0. Every one of the remaining n−1 references has to slide down one slot to close the hole.

list.pop(0) — pull the front, then shift EVERYTHING down one slot before A B C D E F remove ✕ after B C D E F · n−1 moves = O(n) per dequeue
The hidden cost — a list has no movable front, so removing index 0 slides all n−1 survivors down one slot (a single memmove under the hood). Cheap for six items; ruinous for a million.

"Slides down one slot" is a bulk memory copy — internally, a single memmove of n−1 pointers. For six items it is invisible. But for a million-item queue, every single dequeue copies almost eight megabytes of references before it can return one value. That is O(n) per removal, which makes draining the whole queue O(n²). We do not have to trust the reasoning, because we can time it. Here each trial dequeues one item from a size-n queue and re-enqueues it, so the size stays fixed and we isolate the cost of one front removal:

trap.pypython
import timeit
from collections import deque

for n in (10_000, 100_000, 1_000_000):
    tl = timeit.timeit("l.append(l.pop(0))",       # dequeue front, re-enqueue
                       setup=f"l=list(range({n}))", number=5000) / 5000
    td = timeit.timeit("d.append(d.popleft())",    # same, but a deque
                       setup=f"from collections import deque; d=deque(range({n}))",
                       number=5000) / 5000
    print(f"n={n:>9}   list {tl*1e9:8.0f} ns   deque {td*1e9:4.0f} ns")

# n=    10000   list    14790 ns   deque   27 ns
# n=   100000   list   151925 ns   deque   28 ns
# n=  1000000   list   526401 ns   deque   24 ns

Read the list column top to bottom. Ten thousand items: ~15 µs per dequeue. Ten times the items: ~152 µs. That is ten times the cost, a dead-straight line, exactly what O(n) predicts. The deque column beside it never twitches: ~25 nanoseconds whether it holds ten thousand items or a million. (The numbers come from this machine and will vary. The shapes — one climbing, one flat — are the law.) Push it further and it stops being a benchmark and becomes pain. Draining a 100,000-item queue front-to-back took the list 7.5 seconds. The deque did the same job in 5.4 milliseconds, about 1,400× faster, and the gap widens with every extra item.

Those numbers check against each other. Ten times the items gave ten times the per-dequeue cost, 15 µs climbing to 152 µs, which is the straight line O(n) demands — a size-10n queue shifts ten times as many pointers per removal. The full-drain gap is starker still, because there both costs compound: draining 100,000 items is O(n²) work for the list against O(n) for the deque. 7.5 seconds versus 5.4 milliseconds is roughly a 1,400× spread, and every tenfold jump in n widens it by another factor of ten. The deque is not a little faster here. It is a different curve.

↺ The thing people get backwards
list.pop(0) is not "a queue with a small constant" — it is the wrong complexity class. The instinct is that removing one item is cheap because you only wanted one item. But a list stores its front by position, and position 0 must stay position 0, so removing it forces every survivor to move. The cost is not in the item you took; it is in all the items you kept. A queue needs a front that can move instead — and a list's front cannot.

Myth

"A Python list is a perfectly good queue — append and pop(0) do exactly what I need."

Reality

append is fine — amortized O(1), as Volume 3 showed. But pop(0) is O(n): it memmoves the whole tail. You get correct output in quadratic time, and that is the worst kind of bug, because the small tests all pass.

Here is why that bug hides so well. Draining a queue of n items with pop(0) does about n²/2 pointer shifts in total. Run your unit test with a thousand items and that is roughly 500,000 shifts, over in well under a millisecond, so nobody notices. Then you ship it, and production hands you a million items instead — a thousand times more data. But because the work grows with the square, a thousand times the data means a million times more shifts. The test that passed in a blink now grinds for seconds. The code never changed. Only the size did, and O(n²) was waiting the whole time.

The list's sin was a front that couldn't move. What if we let it move — and let both ends wander freely around a fixed block of slots? →

03The ring buffer — two indices chasing around a fixed array

Here is the human insight that fixes everything, and it is beautifully cheap. The whole problem was that dequeuing tried to keep the front pinned at index 0, so don't pin it. Keep the same contiguous array, but let front and back be movable integer indices. Dequeue? Just advance front by one, and the old slot is now dead space we simply don't care about. Enqueue? Write at back and advance back by one. No element ever moves. Both operations are a write and an add, which is genuine O(1).

Walk the indices once and you will feel the problem coming. Take a fixed array of capacity 4, with front and back both starting at 0. Enqueue three items and back climbs 0 → 1 → 2 → 3. Now dequeue two and front climbs 0 → 1 → 2. The live queue is just the one item in slot 2, yet back is already sitting at the last slot. Enqueue once more: that fills slot 3, and back tries to step to slot 4, which does not exist. Slots 0 and 1 sit empty and wasted behind us. The indices only crawl forward, so they run out of room while the array is nearly empty. That is the exact wall the next idea climbs over.

But if both indices only ever crawl forward, they march off the end of a fixed array. The fix is the trick that names the structure: wrap around. When an index reaches the last slot, its next step goes back to slot 0. We compute that with a single modulo, (i + 1) % capacity. The array is no longer a line. It is a ring. The live queue is the arc of slots from front up to back. That arc drifts around the circle as items come and go, forever reusing the slots that dequeues freed behind it. This is a ring buffer, also called a circular buffer, and it is one of the most-used structures in all of systems programming.

Watch the wrap happen with real numbers. Take a ring of capacity 8, so the valid slots are 0 through 7. If back is sitting at slot 7 and a new item arrives, we compute (7 + 1) % 8, which is 8 % 8 = 0 — back lands on slot 0, the slot some earlier dequeue freed. One item later it steps to (0 + 1) % 8 = 1, then 2, and on around the circle. The modulo is the whole ring in one operator: it turns a straight line of indices into a loop that never runs out, as long as the live count stays under the capacity.

InteractiveEnqueue and dequeue — watch front & back chase around the ring
ring buffer capacity 8 0 elements moved
front 0 · back 0
Every op is one write and one index = (index + 1) % 8. Nothing ever slides — the "moved" counter stays at zero no matter how long you play.
· · C D E · · · front back (i + 1) % capacity — the ends are joined
A line bent into a ring — the live queue is the shaded arc from front to back. When either index steps off the end it wraps to slot 0. Freed slots behind front get reused, so a fixed array serves an endless stream — no growth, no shifting.
Wait —
if dequeued slots get quietly reused, and front can equal back in two different situations, how does the ring ever know whether it is empty or completely full?
The deeper cut — how do you tell "full" from "empty"?

There is a classic ambiguity in a ring buffer. When front == back, is the ring empty or completely full? Both states can produce equal indices. Real implementations pick one of three fixes. They keep an explicit count of live items, which is what the widget above does, and it is the simplest and clearest option. Or they waste one slot, so a full ring has back one short of front. Or they use free-running indices that only get masked when you read them. Here is the point worth carrying away. A ring buffer trades unbounded growth for a fixed capacity, and that boundedness is a feature. It is how audio drivers, network stacks, and log pipelines guarantee they will never blow up memory under load. When the ring fills, they apply backpressure, which makes the producer wait, or they drop the oldest item. That is a deliberate choice, not a crash.

Make that bound concrete. A sound card wants a steady stream of samples at 48,000 per second, and it reads them from a ring buffer the driver keeps refilling. Give that ring 4,096 slots and it holds about 85 milliseconds of audio — 4,096 ÷ 48,000 is 0.085 seconds. That fixed 85 ms is the driver's safety margin. If your program falls behind and the ring drains empty, you hear a click. If it races ahead and the ring fills, the driver makes it wait — that is backpressure in the flesh. The buffer is deliberately small and bounded, and that boundedness is exactly what keeps the audio smooth and the memory flat.

A ring buffer is perfect when you know the capacity up front. But Python's queue of choice grows without bound and still gives O(1) at both ends. How? →

04collections.deque — the real fix, and its memory

You will rarely hand-roll a ring buffer in Python. You will reach for collections.deque (pronounced "deck," from double-ended queue), the standard library's answer to exactly this problem. It gives you O(1) append and pop at the back and O(1) appendleft and popleft at the front — no shifting, ever. For a FIFO queue you use just two of its four doors: append to enqueue, popleft to dequeue.

deque.pypython
from collections import deque

q = deque()          # an empty FIFO
q.append("A")        # enqueue at the back
q.append("B")
q.append("C")
first = q.popleft()  # dequeue from the front  ->  "A"  (first in, first out)
print(first, list(q))
# A ['B', 'C']

Line by line: deque() builds an empty double-ended queue. Three appends enqueue A, B, C at the back, in order. Then popleft() removes from the front and returns "A", the oldest item, exactly FIFO, and what remains is B then C. These are the same five lines a list would use, minus the O(n) landmine.

THE STDLIB TOOLBELT · collections.deque as a queueone import, two methods — and the O(n) landmine is gone
from collections import deque q = deque() # an empty FIFO -- that is the whole constructor q = deque(iterable) # or start it full, front-to-back q = deque(iterable, maxlen=n) # bounded: the far end falls off, silently q.append(item) # ENQUEUE at the back -- O(1) item = q.popleft() # DEQUEUE from the front -- O(1) nxt = q[0] # peek the front, leaving it in place if not q: … # ALWAYS ask this before you popleft while q: … # the drain -- and the outer loop of every BFS
append(x)Enqueue. One reference written into the end block's next free cell, one cursor step. The million items already inside are never read — that is where the O(1) comes from.
popleft()Dequeue. Reads the front cell, steps the cursor inward, returns the oldest item. This is the method a list simply does not have, and the reason this box exists.
q[0] · q[-1]Peek the front, peek the back. Both O(1) — measured at 17.9 ns on a 100,000-item deque. Every index between them walks the block chain: the next chapter's honest caveat.
maxlen=nA bounded queue. Once it holds n items, every append drops one off the far end to make room. No exception, no warning, nothing returned — see the second tripwire before you trust it.
queue.QueueThe threaded cousin (Chapter 27). Same FIFO, but get() blocks until an item arrives instead of raising, and put() can block when full. Reach for it when producer and consumer are separate threads; a deque is the tool when one thread owns the queue — and the third tripwire draws that line exactly.
while q:The drain loop. An empty deque is falsy, so this reads as “while anyone is still waiting” — and it is literally the outer loop of the breadth-first search in section 05.
INPUTfrom collections import deque

orders = deque()                       # the whole constructor

orders.append("#1041 latte")           # enqueue at the BACK
orders.append("#1042 flat white")
orders.append("#1043 cortado")
print("waiting:", len(orders), list(orders))

while orders:                          # the emptiness guard, same shape as a stack's
    ticket = orders.popleft()          # dequeue from the FRONT - oldest first
    print("  serving", ticket, "| still waiting:", list(orders))

print("empty?", not orders)
try:
    orders.popleft()
except IndexError as e:
    print("popleft on empty:", type(e).__name__ + ":", e)

recent = deque(maxlen=3)               # a bounded queue SILENTLY drops
for t in ["#1041", "#1042", "#1043", "#1044"]:
    recent.append(t)
    print("  maxlen=3 ->", list(recent))
OUTPUTwaiting: 3 ['#1041 latte', '#1042 flat white', '#1043 cortado']
  serving #1041 latte | still waiting: ['#1042 flat white', '#1043 cortado']
  serving #1042 flat white | still waiting: ['#1043 cortado']
  serving #1043 cortado | still waiting: []
empty? True
popleft on empty: IndexError: pop from an empty deque
  maxlen=3 -> ['#1041']
  maxlen=3 -> ['#1041', '#1042']
  maxlen=3 -> ['#1041', '#1042', '#1043']
  maxlen=3 -> ['#1042', '#1043', '#1044']
TRIPWIRES
  • list.pop(0) is not a slow dequeue — it is the wrong complexity class. Draining 100,000 items on this machine, just now: 4875.1 ms for while q: q.pop(0) against 9.7 ms for while q: q.popleft()501×. Chapter 48 clocked the same contrast at 3005.3 ms versus 10.3 ms; the ratio moves with the machine and the day, the two curves never do.
  • maxlen drops silently, and from the end you did not push. On deque([1,2,3], maxlen=3): append(4) gives [2, 3, 4] — dropped from the left; appendleft(0) then gives [0, 2, 3] — dropped from the right. Nothing is raised and nothing is returned, so the discarded item is gone for good. Use it when forgetting is the feature, never as a size check.
  • An empty deque raises: IndexError: pop from an empty deque (a list says pop from empty list — different words, same lesson). And if q: q.popleft() is safe in one thread only. A deque's individual appends and pops are atomic, but the check and the pop are two separate steps, so another thread can empty the deque in between. That gap is precisely why queue.Queue exists: q.get(timeout=0.25) raised _queue.Empty after 0.25 s of waiting, where the deque raised instantly.

One caution before we look inside. A deque has four doors, and only one pair gives you a queue. append then popleft is FIFO: in at the back, out at the front, oldest first. Flip to append then pop — both at the same end — and you have rebuilt the stack from Chapter 51, newest first. Same object, opposite discipline. So the FIFO promise does not live in the deque itself — it lives in which two methods you choose to call. Pick the pair that matches the order you need, and never mix them without meaning to.

What does a deque look like in memory, and why does it stay O(1) at both ends? Not one contiguous array — that would re-create the list's problem at one end or the other. Instead a deque is a doubly linked list of fixed-size blocks. Each block is a small contiguous array (64 reference slots in CPython), and the blocks are chained with next/prev pointers. The deque keeps direct pointers to the first and last block, plus the write position inside each. Adding at either end writes into the end block; when an end block fills, a new block is linked on. No element is ever copied to make room — growth just links another block.

Put a number on those blocks. A CPython deque block holds 64 references, so a deque of 1,000 items needs at least 1,000 ÷ 64 ≈ 15.6 blocks — round up to 16 — chained together. Fifteen of them are full, holding 960 items, and the sixteenth carries the last 40. Each block adds one next and one prev pointer, so the linking overhead is just 16 × 2 = 32 extra pointers for the entire 1,000-item deque. That is why the per-block bookkeeping is a rounding error beside the references themselves: a handful of pointers guarding thousands of slots.

collections.deque — a doubly linked chain of fixed blocks (64 slots each) deque left / right ptrs ·ABC block 0 · 64 slots DE·· block 1 · 64 slots next / prev front → popleft back → append
Why both ends are O(1) — the deque holds direct pointers to its end blocks. popleft reads the front block; append writes the back block; a full end block just links a fresh one. Elements never move — so cost never depends on how many are already inside. (The 64-slot block design is the next chapter's main event.)

That design has a memory price worth naming out loud. An empty list weighs 56 bytes, while an empty deque weighs 760 bytes, because it pre-allocates its first block of slots up front. So for tiny collections a deque is heavier. But per element the two converge on the same figure, about 8 bytes each, and that is because both, at bottom, store 8-byte references (Vol 1) rather than the objects themselves:

deque.pypython
import sys
from collections import deque
print(sys.getsizeof([]),        sys.getsizeof(deque()))       # empty:  56   760
for n in (1000, 10000):
    print(n, sys.getsizeof(list(range(n))), sys.getsizeof(deque(range(n))))
# 1000    8056    8680
# 10000  80056   83128

At 10,000 items the list holds 80,056 bytes and the deque 83,128. The deque's small surplus is the per-block bookkeeping: the next/prev pointers and the partly-filled end blocks. Both are ~8 bytes times n plus a fixed header, each a container of references, precisely the Volume 1 model. So the deque buys you O(1) at both ends for a modest, near-constant space premium, and that is a very good trade.

You can check that 8-bytes-plus-header claim by hand. A reference is 8 bytes, and the list holds 10,000 of them, so the slots alone are 10,000 × 8 = 80,000 bytes. Add the list's fixed 56-byte header and you get 80,056 — exactly the number the tool reported. Nothing mysterious is hiding in there. The list is a header stapled to a flat run of references, just as Volume 1 drew it. The deque's 83,128 is the same story with a little more bookkeeping: the same 80,000 bytes of references, plus the next/prev pointers and the slack in its partly-filled end blocks.

InteractiveSlide n — one queue's cost climbs, the other's stays flat
time for ONE dequeue from a queue of n items list.pop(0) O(n) — shifts every survivor down one slot deque.popleft() O(1) — advance a pointer, never move an element
n
Grow the queue and the red bar marches with n while the green bar refuses to move. That flat green line is the whole reason deque exists.

You now have a real O(1) FIFO. The last question is the one that makes it worth knowing: where does the machine — and the wider world — actually run on queues? →

05How the machine feels it, and why search runs on queues

Big-O says the ring buffer and a naive pointer-per-node linked queue are both O(1) per operation. The metal disagrees about which is fast. A ring buffer is one contiguous array, so consecutive slots sit in consecutive cache lines. Walking the queue streams through the CPU cache (Volume 3) at full speed. A linked queue puts one heap node per item, scattered anywhere in memory. So every dequeue chases a pointer to an address the cache never predicted. That is a stall of hundreds of cycles, again and again. The deque splits the difference on purpose. 64 items per block means you get long contiguous runs, which are cache-friendly, with only an occasional pointer hop between blocks. That block size is not arbitrary. It is tuned so the pointer-chase is rare relative to the streaming. This is the whole reason a heap lives in a flat array too, a lesson the heap chapter makes central.

BFS (Vol 3 ch 13): the queue IS the frontier — explore level by level s a b c d frontier queue (FIFO): front a b · back dequeue s → discover a, b → enqueue them → dequeue a → discover c … FIFO order guarantees nearer nodes are always visited first. Swap the queue for a stack, and the very same loop becomes DFS.
The famous one — breadth-first search is just "process the frontier in FIFO order." Because the queue serves oldest-first, BFS fans out in rings of equal distance from the start — which is exactly why it finds shortest paths in an unweighted graph.

That is the deepest "wow" in the chapter: BFS and a queue are the same idea. The queue holds the frontier, the nodes seen but not yet explored. Dequeue the oldest, enqueue its undiscovered neighbours at the back, and repeat. Because a queue is FIFO, everything one step from the start is processed before anything two steps away. So BFS sweeps outward in perfect concentric rings and lands on the shortest path first. Now change one line to dequeue from a stack instead, and the identical loop becomes depth-first search. The container chooses the algorithm. That is the 1% move in miniature: the data structure is the strategy.

Picture it on a tiny grid to see the rings. Start at cell S and enqueue it. Dequeue S, then enqueue its four neighbours — the cells exactly one step away. Because they all sit at the back, the queue empties every distance-1 cell before it touches a distance-2 cell. When you finally dequeue a distance-1 cell and enqueue its undiscovered neighbours, those are distance-2, and they line up behind everything closer. So the search always finishes a whole ring before it starts the next. The first time you reach your target, you reached it in the fewest steps possible — that is why BFS finds the shortest path, and it is the FIFO rule doing every bit of the work.

Count the rings and the shortest-path claim becomes something you can check. Ring 0 is just S itself, the start. Ring 1 is its 4 neighbours, one step out. Ring 2 is everything one step beyond those, two steps from S, and so the pattern goes. Because the queue drains ring 0 completely, then ring 1, then ring 2, the moment a target pops out of the queue its ring number is its distance from S in steps. If your target sits in ring 3, BFS reached it in 3 steps and could not have done it in fewer — every earlier ring held the cells that were closer, and all of them came out first.

InteractiveSlide the search deeper — BFS’s queue explodes, DFS’s stack barely grows
peak memory a search must hold · tree branches b = 4 ways · explored to depth d BFS · queue holds the frontier O(bᵈ) — one whole level, each b× wider than the last · DFS · stack holds one path O(b·d) — at most b siblings per level on the path · Same loop, same goal found — but the queue keeps the entire outer ring; the stack keeps only the trail. Swap the queue for a stack and BFS becomes DFS — the container also chooses the memory bill.
depth 6
Breadth-first search must remember every node on the current frontier at once — and each level is b times wider than the last, so the queue climbs to tens of millions by depth 12. Depth-first search never holds more than one root-to-leaf path. Same search, same shortest path found — wildly different memory.

06When to reach for a queue

Which item comes out next? = which end do you touch? oldest first — FIFO QUEUE deque.append / .popleft fairness · buffering · BFS · schedulers newest first STACK list.append / .pop undo · DFS · ch 03 both ends DEQUE appendleft / popleft / … sliding windows · ch 05
Pick it on sight — the whole choice collapses to one question: which end comes out next? Oldest-first is a queue; newest-first is a stack; if you need both, it is a deque. Nothing here should ever be a list's pop(0).

Good at data structures is mostly one skill: matching the access pattern to the container on sight. The queue's tell is unmistakable — whenever items must be handled in arrival order, and you always take the oldest first. Fairness (serve who came first) or buffering (a fast source, a slow drain, order preserved between them). The instant you catch yourself writing list.pop(0) in a loop, stop: you have found a queue wearing a list's clothes, and it is quietly O(n²). Type from collections import deque instead.

WATCH AN EXPERT THINKa hundred a minute — and the burst that obeys the rule while breaking the promise
A public API with one written promise: at most 100 requests per 60 seconds from any one key. Every arriving request needs an allow-or-deny answer before it is served, which means this check runs more often than anything else you will ever write — once per request, forever, standing in front of the real work. Make it concrete: 60,000 requests across an hour on a single key, and a cap that somebody may raise or lower on a Tuesday afternoon without telling you. Nothing in that wording says queue. It says count what happened recently. So rewind to before you knew the answer and watch somebody get there from cold, because on this problem the wrong turn is not slow at all — it is fast, and it is quiet.
FIRST INSTINCT
Transcribe the promise literally. Keep the timestamps you let through; on each new request throw away the ones older than sixty seconds and count what is left — kept = [h for h in kept if h > t - 60], then admit if len(kept) < cap. That is the sentence from the contract, in Python, and being obviously right is worth more than it sounds: it is what lets this version hold the answer key later, the same job chapter 51's referee did. It is also wasteful in a way you can name precisely. Every single request rebuilds a list of up to cap survivors, so the bill grows with the cap — and that is the wrong quantity for it to grow with, because raising a customer's limit is the friendliest act in the whole system. Measured below, same 60,000 requests each time: 81.9 ms at a cap of 50, 1528.5 ms at a cap of 1,000. Multiply the cap by four and the cost multiplies by 4.1; by five, and by 4.6.
WHY IT STALLS
So make the repair almost everyone makes, and it is a genuinely good one: stop storing timestamps at all. Chop the clock into fixed sixty-second slots, keep one integer per slot, admit while that integer is under the cap. One counter, O(1) per request, 9.5 ms where the honest version took a second and a half. And it agrees: on 500 requests of ordinary traffic all three limiters admit all 500, and an audit of the busiest sixty seconds found anywhere in the stream reports 61 for every one of them. Three programs, one answer. That is exactly what makes it a trap. Ask instead what shape a slot boundary cannot see, and the answer needs no cleverness — 100 requests in the last tenth of one minute, 100 in the first tenth of the next. Each slot counts exactly 100. Each slot is within its limit. The counter reports full compliance, and 200 requests went through inside a fifth of a second under a promise of 100 per minute; the audit rows below put the worst sixty-second span at 200, precisely double. The lesson is not that counters are bad — a fixed window is a legitimate, widely shipped design. It is that “it agrees on my traffic” and “it keeps the promise” are two different claims, and on a rate limiter the traffic is chosen by whoever wants past you.
THE TELL
Stop asking how to count what is inside the window, and ask which request leaves it next. It is always the oldest one still in there. Always — no comparison, no tie to break, no exception — because requests arrive in time order, so the order they expire is the order they arrived. That one sentence removes both hard parts at once: nothing needs sorting, because they came sorted; nothing needs searching, because the one about to go is at the front by construction. What is left is a queue and nothing more than a queue. Drop the expired from the front, then decide: while q and q[0] <= t - 60: q.popleft(), and if len(q) < cap: q.append(t). Now count the doors that costs. In at the back, out at the front, one peek at q[0], one while q guard — and nothing is ever removed from the back, because this problem never once asks for it. The while loop only looks unbounded: each admitted timestamp is appended once and dropped once across the whole hour, so the popping is bounded by the admitting, and the cap has left the cost entirely. Volume 3's chapter 38 named this in passing as a window of timestamps sliding with the clock. The queue is the container that makes the sliding free.
from collections import deque
import random, time

WINDOW = 60.0                           # the promise: at most `cap` requests per 60 seconds

def by_filter(stamps, cap):             # the instinct -- rebuild the window on every request
    kept, ok = [], []
    for t in stamps:
        kept = [h for h in kept if h > t - WINDOW]
        if len(kept) < cap:
            kept.append(t); ok.append(t)
    return ok

def by_bucket(stamps, cap):             # the near-miss -- one counter per fixed 60-second slot
    counts, ok = {}, []
    for t in stamps:
        slot = int(t // WINDOW)
        if counts.get(slot, 0) < cap:
            counts[slot] = counts.get(slot, 0) + 1
            ok.append(t)
    return ok

def by_queue(stamps, cap):              # the tell, executed
    q, ok = deque(), []
    for t in stamps:
        while q and q[0] <= t - WINDOW: # they expire in the order they arrived -- always
            q.popleft()
        if len(q) < cap:
            q.append(t); ok.append(t)
    return ok

def worst_span(ok):                     # the audit: most admitted inside ANY 60-second window
    w, worst = deque(), 0
    for t in ok:
        w.append(t)
        while w[0] <= t - WINDOW:
            w.popleft()
        worst = max(worst, len(w))
    return worst

random.seed(11)
quiet = sorted(random.uniform(0, 600) for _ in range(500))      # ~0.8/s, nowhere near the cap
burst = ([59.900 + i / 1000 for i in range(100)] +              # 100 in the last tenth of a minute
         [60.000 + i / 1000 for i in range(100)])               # 100 in the first tenth of the next
for label, stream in (("quiet stream", quiet), ("boundary burst", burst)):
    print(f"{label}: {len(stream)} requests, promise = 100 per 60 s")
    for name, fn in (("filter", by_filter), ("bucket", by_bucket), ("queue ", by_queue)):
        ok = fn(stream, 100)
        print(f"    {name}  admitted {len(ok):4d}   worst 60 s span {worst_span(ok):4d}")

heavy = sorted(random.uniform(0, 3600) for _ in range(60_000))  # an hour of traffic, one key
print("60,000 requests over an hour       filter    bucket     queue")
for cap in (50, 200, 1000):
    ms = []
    for fn in (by_filter, by_bucket, by_queue):
        t0 = time.perf_counter(); ok = fn(heavy, cap); ms.append((time.perf_counter() - t0) * 1e3)
    print(f"  cap={cap:<5} {len(ok):6,} admitted  {ms[0]:8.1f} {ms[1]:9.1f} {ms[2]:9.1f}  ms")
print("  filter and queue admit the very same requests at every cap:",
      all(by_filter(heavy, c) == by_queue(heavy, c) for c in (50, 200, 1000)))
quiet stream: 500 requests, promise = 100 per 60 s
    filter  admitted  500   worst 60 s span   61
    bucket  admitted  500   worst 60 s span   61
    queue   admitted  500   worst 60 s span   61
boundary burst: 200 requests, promise = 100 per 60 s
    filter  admitted  100   worst 60 s span  100
    bucket  admitted  200   worst 60 s span  200
    queue   admitted  100   worst 60 s span  100
60,000 requests over an hour       filter    bucket     queue
  cap=50     3,000 admitted      81.9       5.0       3.3  ms
  cap=200   12,000 admitted     333.9       6.3       4.6  ms
  cap=1000  58,619 admitted    1528.5       9.5       9.2  ms
  filter and queue admit the very same requests at every cap: True
trigger: reach for a FIFO queue whenever things expire in the order they arrived — the instant you can say “the next one to leave is the oldest one still here”, there is nothing to sort and nothing to search, and the entire implementation is append, popleft, and a peek at q[0]. Read the timing table as shape, not as milliseconds — run it again and the millisecond column shifts by a few percent while the relationship between the columns does not, which is why the burst rows above are stated in whole admitted requests instead. The filter row tracks the cap almost exactly, which is the defect itself. The queue row does move — 3.3 to 9.2 ms — so be honest about what it is tracking: admissions went from 3,000 to 58,619, nineteenfold, and the cap twentyfold, while the time went up less than threefold. It pays per request handled, a little more per request admitted, and nothing at all for the size of the cap. The habit underneath is the one this chapter can actually prove: test the shape you are afraid of, not the shape you happen to have. The quiet stream said all three limiters were the same program. It took a hand-built fifth of a second to tell them apart — and on a rate limiter that shape is not hypothetical, because the input is chosen by someone who wants past you. Write the adversarial case first; the ordinary one will never fail you, and it will never tell you anything either. One honest caveat, because the queue is not free: it holds up to cap timestamps for every key, where the counter holds a single integer, and across a million keys that is a real memory bill. Plenty of shipped limiters take the counter and accept the doubled burst deliberately, and that is a sound trade. What is not sound is arriving there by accident and still calling it 100 per minute.
NOW BUILD A QUEUE OUT OF TWO STACKSboth of them serve the newest — stand them nose to nose and they serve the oldest
The drill. Forget deque exists. You are allowed only the two stack moves from Chapter 51 — append and pop, the far end of a list — and you must build a working FIFO out of them. No pop(0), no insert(0, x), no shifting.

The one idea that unlocks it: keep two stacks. Arrivals land on _in. Departures leave from _out. When _out runs dry, pour _in into it one item at a time — and notice what pouring a stack into a stack does to the order.

Then answer the real question. That pour is O(n), so how can this be a constant-time queue? The trick is when you are allowed to pour. Write the rule down before you code it: pour only when _out is empty, never on a whim.

The check that settles it: do not time it — count it. Instrument the pour, push 100,000 items through, drain them all, and print the total number of items moved. If your rule is right, that number lands on something exact and beautiful. Predict it before you run it.
show the solution
class TwoStackQueue:
    """A FIFO queue built from two LIFO stacks - and nothing else."""

    def __init__(self):
        self._in = []            # arrivals land here, newest on top
        self._out = []           # departures leave from here, OLDEST on top

    def enqueue(self, x):
        self._in.append(x)       # always O(1)

    def _flip(self):
        # Pour _in into _out. Popping a stack reverses it, so the oldest
        # arrival - buried at the bottom of _in - ends up on TOP of _out.
        while self._in:
            self._out.append(self._in.pop())

    def dequeue(self):
        if not self._out:        # only refill when _out has run dry
            if not self._in:
                raise IndexError("dequeue from empty queue")
            self._flip()
        return self._out.pop()   # O(1) on every call that finds _out stocked

    def __len__(self):
        return len(self._in) + len(self._out)


q = TwoStackQueue()
for t in ["A", "B", "C"]:
    q.enqueue(t)
print("in:", q._in, "out:", q._out)          # nothing has flipped yet
print("dequeue ->", q.dequeue())             # this one pays for the flip
print("in:", q._in, "out:", q._out)
print("dequeue ->", q.dequeue())             # free
q.enqueue("D")                               # arrives on the IN side
print("in:", q._in, "out:", q._out)
print("dequeue ->", q.dequeue())             # still free - out was stocked
print("dequeue ->", q.dequeue())             # out empty -> flip D across
print("len:", len(q))
try:
    q.dequeue()
except IndexError as e:
    print("empty:", type(e).__name__ + ":", e)

# in: ['A', 'B', 'C'] out: []
# dequeue -> A
# in: [] out: ['C', 'B']
# dequeue -> B
# in: ['D'] out: ['C']
# dequeue -> C
# dequeue -> D
# len: 0
# empty: IndexError: dequeue from empty queue

# THE AMORTIZED PROOF: count the moves, not the clock.
moves = 0
class Counted(TwoStackQueue):
    def _flip(self):
        global moves
        while self._in:
            self._out.append(self._in.pop()); moves += 1

c = Counted()
for i in range(100_000):
    c.enqueue(i)
for i in range(100_000):
    c.dequeue()
print("100,000 items ->", moves, "flip moves total =", moves / 100_000, "per item")

# 100,000 items -> 100000 flip moves total = 1.0 per item

# WHY THE FLIP IS RARE - and why "1.0" is the whole answer.
# Follow one item all the way through. It is pushed onto _in exactly once.
# It is moved across to _out exactly once. It is popped off _out exactly
# once. Three touches, fixed, forever - no matter how many other items are
# in the queue when its turn comes. That is why the counter printed exactly
# 100,000 moves for 100,000 items: 1.0 each, not 1.4, not n.
#
# The pour LOOKS expensive because one unlucky dequeue moves everything
# waiting. But that dequeue is buying the free ride for every dequeue behind
# it - _out is now stocked, and the next k calls just pop. Charge the cost
# to the items rather than to the calls and it flattens to a constant: this
# is amortized O(1), the exact bookkeeping the list's overallocation used.
#
# Break the rule and watch it die. Drop the `if not self._out:` guard so
# every dequeue re-pours the whole queue, and count again at n = 10,000:
#
#   guarded        10,000 moves  (      1.0 per item)
#   unguarded 100,000,000 moves  ( 10,000.0 per item)
#
# That is n moves against n**2 - measured, not estimated - and you have
# rebuilt list.pop(0) out of two stacks. The two stacks were never the
# trick. The GUARD was.
#
# Worth knowing where you have seen this: two stacks make a queue, and the
# same trick in reverse (two queues make a stack) is the classic pair. The
# structure was never in the container. It was always in the discipline.
Where you meet this — every day, mostly invisibly
Your OS scheduler holds ready processes in run queues and serves them fairly. Every print job waits in a print queue. BFS (maze solvers, "degrees of separation," network broadcast, the shortest path your GPS considers on an unweighted grid) runs on a queue. Task/message queues — Celery, RabbitMQ, Amazon SQS, Kafka — are literally this structure at datacenter scale: producers enqueue jobs, worker fleets dequeue them in order. Keyboard and network input buffers queue your keystrokes and arriving packets so nothing is lost or reordered while the program is busy. Streaming audio and video buffer incoming frames in a ring so playback stays smooth when the network stutters. You used several of these to load this page.
The rule of thumb
Need oldest-first (FIFO)? → collections.deque, using append and popleft. Need newest-first (LIFO)? → a stack (a plain list — the previous chapter). Need both ends, or a fixed-capacity buffer? → still a deque, or a ring buffer. Never a list's pop(0) for a queue.

You have the FIFO half of the story. But notice the widget above did something a pure queue never needs: it added and removed at both ends. That double-ended powerappendleft as easily as append — is not a queue at all. It is the deque in full, and its 64-slot block design is a small marvel of engineering worth a chapter of its own.

Next (chapter 53): the deque — one structure that is a stack and a queue at once. We open up those linked blocks of 64, see why that exact number, and watch appendleft and popleft stay O(1) at the end nobody else can touch. →

SAY IT BACKthe chapter in five breaths
  1. A queue is one law with two doors — in at the back, out at the front, and the front is never a decision — so the item leaving is always simply the one that has waited longest, arrival order survives the whole trip untouched, and that single property is what the words fairness and buffering both mean once they are written in code.
  2. The obvious Python queue is a lie of omission: append is fine, but a list's front is pinned at index 0 with no marker to advance, so pop(0) memmoves every survivor down one slot — O(n) per dequeue, O(n²) to drain — and the test suite always passes, because the cost only becomes visible at a size you never test at.
  3. The fix is to stop pinning the front: let two integers wander instead and wrap them with (i + 1) % capacity, and a fixed array becomes a ring that serves an endless stream while no element ever moves — deliberately bounded, which is how audio drivers and network stacks promise never to blow up under load, applying backpressure or dropping instead.
  4. collections.deque is that idea shipped: a doubly linked chain of 64-slot blocks, so both ends are genuine O(1) with long contiguous runs and only an occasional pointer hop — 760 bytes empty against a list's 56, then converging on the same ~8 bytes an item — and yet none of that engineering is what makes it a queue, because a deque opens all four doors and section 04 is where you choose which two you are willing to touch.
  5. Once you see it you cannot unsee it: breadth-first search is a queue — hold the frontier, serve the oldest, enqueue its undiscovered neighbours — so the search finishes each ring of equal distance before starting the next, which is precisely why it finds the shortest path; and swap that queue for a stack and the identical loop becomes depth-first, because the container is the strategy.
reach for this when… section 06 gave you the question that selects a queue; here are the two counter-tests that stop you over-reaching with one, because both failures are quiet rather than loud. Does anything ever deserve to jump the line? Urgent first, cheapest first, nearest first — if yes it was never FIFO, it is a priority queue, and it wants a heap. Do I ever need to take something back off the end I just added to? An undo history with a ceiling does — newest off one end to undo, oldest off the other to stay bounded — and so does any pool that serves two different consumers from two different ends. If yes, you have outgrown one door and want both, which is the deque in full and the next chapter. If both answers are no, listen to the words the job already arrived in: in the order they came, whoever has waited longest, while the worker catches up, level by level. Every one of those is FIFO in ordinary clothes, and the entire implementation is from collections import deque with append, popleft, q[0] to peek, and a while q: guard standing between you and an IndexError.
You already owned the pieces: chapter 48 gave you the definition — a data structure is an arrangement plus the operations that arrangement makes cheap — and a queue is that definition wearing its rule on the outside; chapter 49 gave you the contiguous block of 8-byte references, which is at once the ring buffer's entire body and the reason pop(0) has to shuffle; chapter 50 gave you nodes and pointers, so the deque's chain of blocks was yours before you met it, and so was the reason a block of 64 beats a node per item; chapter 51 gave you the stack, and with it the habit of holding a plain container to a single promise — this chapter only moved the promise to the other end. Further back: Volume 1's chapter 6 gave you the list as a header stapled to a flat run of references, which is why 10,000 items measured exactly 80,056 bytes with nothing hiding in there; Volume 3's chapter 2 gave you the cache line that rewards contiguity, the reason Big-O calls the ring and the linked queue equal while the metal does not; Volume 3's chapter 3 gave you amortized accounting, which is why the two-stack queue's expensive-looking pour came out at exactly 1.0 moves per item; Volume 3's chapter 13 gave you breadth-first search, which turns out to have been carrying a queue the whole time; and chapter 27 gave you threads, the one condition under which if q: q.popleft() stops being safe and queue.Queue takes over. Chapter 52 added a single sentence — the oldest waiting item leaves first — and then spent six sections making that sentence cost nothing.
PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 52, in working code

A queue is the most patient structure there is — whoever waited longest gets served first — and once you see two little indices (or one clever deque) make that both fair and fast, you'll spot queues humming underneath every scheduler, print spooler, and search you ever meet.

The FIFO idea — first in, first out
A queue only ever adds at the back and removes from the front. The order out matches the order in — always.
Roll your own — the ring buffer
A fixed array, two indices, and modulo arithmetic. The front is allowed to MOVE, so nothing ever shifts.
The everyday queue — collections.deque
In real code you reach for deque, not a hand-rolled ring: O(1) at both ends, and it grows without bound.
What a queue is really for — breadth-first search
Serve the oldest first and a search fans out level by level. Swap the queue for a stack and the SAME code goes deep instead.
end of chapter 52 · six sections
iolinked
designed & developed by Ajai Raj
Python, from the metal up · a product of iolinked