Stacks, Queues, and Deques: A Practical Deep Dive into collections.deque

Lists make deceptively bad stacks and queues. Here's why collections.deque exists, when to reach for it, and how it behaves under the hood.

Ask a room full of Python beginners to build a queue and most of them reach for a plain list. It works — right up until it doesn't. Once your data structure grows, or you start removing items from the front in a hot loop, the humble list quietly turns into a performance trap. This is where collections.deque earns its place in the standard library.

Before we go deep on the implementation, it's worth grounding the concepts themselves. If you want a clear, from-scratch explanation of what stacks and queues actually are — LIFO versus FIFO, push and pop, the mental models — our friends over at meine-codereise.de wrote an excellent German-language primer: Stacks und Queues in Python: Zwei fundamentale Datenstrukturen. This article picks up where that one leaves off and focuses on the tool Python gives you to implement both efficiently.

Why a list is the wrong queue

A Python list is a dynamic array. Appending to the end is amortized O(1), and so is popping from the end — that part is fine, and it's exactly why a list makes a perfectly good stack. The problem is the front. When you call list.pop(0) or list.insert(0, x), every remaining element has to shift by one position in memory. That's an O(n) operation. Do it inside a loop and your queue degrades to O(n²) overall.

import time

data = list(range(1_000_000))

start = time.perf_counter()
while data:
    data.pop(0)   # O(n) every single time
print(time.perf_counter() - start)   # painfully slow

Run that and go make a coffee. The same workload with a deque finishes almost instantly.

Enter collections.deque

A deque (pronounced "deck", short for double-ended queue) is implemented as a doubly-linked list of fixed-size blocks. That design gives you O(1) appends and pops at both ends:

from collections import deque

q = deque()
q.append(10)        # add to the right
q.appendleft(20)    # add to the left
q.pop()             # remove from the right -> 10
q.popleft()         # remove from the left  -> 20

For a FIFO queue, the pattern is append() to enqueue and popleft() to dequeue. For a LIFO stack, use append() and pop() — though for a pure stack a list is equally fine.

The features people miss

Two of deque's best features aren't obvious from the name. The first is maxlen, which turns a deque into a bounded ring buffer. Once it's full, appending to one end silently drops an element from the other — perfect for "keep the last N things" problems like log tails or moving windows:

recent = deque(maxlen=3)
for item in "abcde":
    recent.append(item)
print(recent)   # deque(['c', 'd', 'e'], maxlen=3)

The second is rotate(), which shifts every element by a given number of steps in O(k) time. It's a surprisingly elegant tool for round-robin scheduling and cyclic buffers:

d = deque([1, 2, 3, 4, 5])
d.rotate(2)     # deque([4, 5, 1, 2, 3])
d.rotate(-1)    # shift the other direction

Know the trade-offs

Deque isn't a free lunch. Because it's block-linked rather than a contiguous array, random access by index is O(n) in the middle — so if you need d[i] in the center of a large collection frequently, a list is the better fit. Deques also aren't slice-able the way lists are. The rule of thumb: reach for a deque when your access pattern lives at the ends, and stick with a list when you need fast indexing in the middle.

One more nicety: deque operations like append, appendleft, pop, and popleft are thread-safe thanks to the GIL, which makes deque a common lightweight choice for simple producer/consumer patterns without reaching for a full queue.Queue.

Wrapping up

Stacks and queues are two of the most fundamental data structures in all of programming, and Python hands you a single, fast, batteries-included type that models both. Understand the concepts first — the meine-codereise.de walkthrough is a great place to solidify them — then let collections.deque do the heavy lifting in production. Your O(n²) queue will thank you.

]]>