skip to content

What happens when you append to a `collections.deque` created with maxlen?

level: middleimportance: should knowfreq 50%

answer

  1. The push wins, something else loses
  2. Which end loses depends on which end pushed
  3. No exception is raised when full
  4. Fixed at construction, read-only afterwards
  5. append evicts from the left end

basics

~20 s

Once the deque is full, every push silently discards an item from the opposite end. append drops the leftmost item, appendleft drops the rightmost, and no exception is raised. maxlen is fixed at construction and read-only afterwards.

solid answer

~50 s

`collections.deque(iterable, maxlen=n)` is a bounded buffer. While `len(d) < n` it behaves normally; once it is full, each `append` discards the item at the left end and each `appendleft` discards the item at the right end — **silently**, with no exception and no callback. `extend` and `extendleft` follow the same rule item by item, so extending a full deque with more than `n` items leaves only the last `n` of them. `d.maxlen` is exposed as a read-only attribute (assigning to it raises `AttributeError`); `maxlen=None` is the default and means unbounded, and `maxlen=0` is legal — the deque simply stays empty. That one constructor argument is the idiomatic fix for a "keep the last N" buffer that would otherwise grow without limit, because eviction happens at push time instead of in a cleanup pass you have to remember to run.

code

python · 11 lines
python
from collections import deque

window = deque(maxlen=3)
for x in range(5):
    window.append(x)
print(list(window))    # [2, 3, 4]

window.appendleft(99)
print(list(window))    # [99, 2, 3]
print(window.maxlen)   # 3
print(len(window) == window.maxlen)  # True

go deeper

for a junior

Recall that collections.deque(maxlen=n) keeps only the newest n items and drops the oldest quietly. Being able to reach for it instead of hand-trimming a list is what is being checked here.

for a middle

Explain the mechanics precisely: which end loses on append versus appendleft, that eviction is silent and O(1), that extend applies it item by item, and that maxlen is fixed at construction and read-only.

for a senior

Show the judgement about when silent dropping is acceptable. Name the alternative when a drop is an event — an explicit fullness check, or a bounded queue.Queue that blocks — and connect the bound to keeping a long-running worker's memory flat.

for a principal

Own the policy question: what is the retention window for in-process buffers, who decides that dropping is safe for this data class, and where the boundary sits between an in-memory tail and a durable store that must not lose records.

## The bounded-buffer contract Passing `maxlen` turns `collections.deque` into a fixed-capacity buffer whose eviction policy is decided for you: **a push at one end that would exceed the bound removes an item from the other end.** The deque never grows past `maxlen`, never raises on a full push, and never tells you that something was dropped. Concretely: ```python from collections import deque d = deque([1, 2, 3], maxlen=3) d.append(4) # -> deque([2, 3, 4]) : the 1 is gone d.appendleft(0) # -> deque([0, 2, 3]) : the 4 is gone ``` The symmetry is the part people get wrong in interviews. `append` evicts from the *left*, `appendleft` evicts from the *right*: the new item always wins, and the loser is the one furthest from where you pushed. `extend` and `extendleft` are defined as repeated single pushes, so they obey the same rule per item; `deque(range(1000), maxlen=5)` is a legal way to keep only the last five values of a long iterable. `maxlen` is read-only after construction — `d.maxlen = 10` raises `AttributeError`. To change the bound you build a new deque from the old one: `d = deque(d, maxlen=10)`. Two edge values matter: `maxlen=None` (the default) means unbounded, so an unbounded deque still reports `d.maxlen is None` rather than a number; and `maxlen=0` is accepted, producing a deque that swallows every push and stays empty, which is occasionally useful as a null sink. ## Why the bound is the point The failure this prevents is unbounded memory growth in a long-running process. Consider a route-optimisation job that keeps every intermediate solution it computes so it can report on recent progress. Written with a list and `results.append(...)`, the collection grows for the lifetime of the run; on a long job the process RSS climbs until the box or the container limit stops it, and the fix people reach for first — an occasional `del results[:-500]` — is a linear copy that they must remember to call from every code path. `results = deque(maxlen=500)` moves the policy into the data structure: eviction is O(1), it happens on the push itself, and there is no cleanup path to forget. The same shape covers a rolling log tail, the last N latency samples, a fixed-size undo trail, and a small cache of recently solved sub-problems. ## What a bounded deque is *not* It is not a cache with lookup. A bounded deque gives you recency-ordered *storage*, not membership testing: `x in d` is a linear scan, and `d[i]` in the middle is O(n) because of the block-chain layout. If a worker wants a hit rate off recent results — say the route job resolves 83% of leg requests from recently computed answers — the deque alone will scan itself on every lookup. Pair it with a dict keyed by the lookup key, or use `functools.lru_cache` when the thing you are caching is a pure function call and you want the eviction and the lookup handled together. It is also not a signal. Because eviction is silent, a bounded deque is the wrong structure when *dropping* is an event someone must react to — a dropped audit record, a dropped payment message. There you want an explicit capacity check that raises or applies backpressure, and for a producer/consumer handoff across threads that is `queue.Queue` with its own `maxsize`, which blocks or raises rather than discarding. The deque's contract is "the newest N matter and the rest do not", and you should say that out loud before choosing it. ## Small details that come up A bounded deque still supports `rotate`, `reverse`, `count`, `index` and iteration exactly as an unbounded one does. `d.insert(i, x)` on a *full* bounded deque raises `IndexError` rather than evicting, because insert has no obvious victim — that asymmetry with `append` surprises people. Copying preserves the bound: `d.copy()` and `copy.copy(d)` return a deque with the same `maxlen`. And `len(d)` is O(1) as always, so checking fullness is `len(d) == d.maxlen`. ## Compared with trimming by hand The alternative people write first is a list plus a trim: `buf.append(x)` followed by `if len(buf) > N: buf.pop(0)`. It is worse on three counts. It is O(n) per trim, because `list.pop(0)` shifts everything down. It is easy to get wrong — `>` versus `>=`, or a trim that runs on one append path but not another, so the bound silently stops holding on the branch nobody tested. And it puts the retention policy in the call sites rather than in the object, so a new caller that appends without trimming reintroduces the growth. `deque(maxlen=N)` moves all three problems into the constructor: the bound is a property of the container, it is enforced on every push from every call site, and the eviction is O(1).

  • How do you find out that a bounded `collections.deque` actually dropped something?
    You do not — eviction is silent by design. If the drop is an event you must observe, check before you push (`len(d) == d.maxlen`) and record or handle the item you are about to displace, or use a structure that signals fullness: `queue.Queue` with a `maxsize` blocks or raises instead of discarding. Choose the deque only when "the last N matter and the rest do not" is genuinely true.
  • How do you change the bound of an existing bounded deque?
    You rebuild it: `d = deque(d, maxlen=new_bound)`. The `maxlen` attribute is read-only, so assignment raises `AttributeError`. Rebuilding copies the items in order and, if the new bound is smaller, keeps only the last `new_bound` of them, because the constructor applies the same eviction rule while consuming the iterable.
  • Does `deque.insert` also evict when the deque is full?
    No. On a bounded deque that is already at `maxlen`, `d.insert(i, x)` raises `IndexError` instead of discarding an item, because there is no natural victim for an insertion in the middle. Only the pushing operations — `append`, `appendleft`, `extend`, `extendleft` and the constructor — evict.

saying these in an interview costs you the question

  • Thinks a full deque raises when you append
  • Expects appendleft to drop from the left end
  • Believes maxlen can be reassigned later
  • Assumes maxlen=0 is an error
  • Treats a bounded deque as a lookup cache
  • Says an unbounded deque has maxlen 0

context