skip to content

Why does `collections.deque.extendleft()` reverse the iterable you pass it?

level: middleimportance: nice to knowfreq 25%

answer

  1. It is defined as repeated single pushes
  2. Each new item displaces the previous front
  3. The last item consumed ends up leftmost
  4. Streaming an iterable rules out buffering it
  5. Pass reversed(items) to keep input order

basics

~20 s

Because extendleft is defined as a series of appendleft calls. Each item is pushed in front of the one before it, so the last item pushed ends up leftmost and the result reads in the opposite order to the input.

solid answer

~40 s

`d.extendleft(it)` is documented as equivalent to `for x in it: d.appendleft(x)`. Every push goes in front of the previous one, so consuming `[4, 5, 6]` leaves the deque starting `6, 5, 4` — the input order reversed. This is not a bug and it is not going to change; it falls straight out of the definition, and any left-extending API that consumes its input one item at a time has the same property. If you want the input order preserved on the left, pass it reversed: `d.extendleft(reversed(items))`, or `d.extendleft(items[::-1])` for a sequence you can slice. `extend` on the right has no such surprise, because appending keeps arrival order.

code

python · 9 lines
python
from collections import deque

d = deque([1, 2, 3])
d.extendleft([4, 5, 6])
print(list(d))   # [6, 5, 4, 1, 2, 3]

d = deque([1, 2, 3])
d.extendleft(reversed([4, 5, 6]))
print(list(d))   # [4, 5, 6, 1, 2, 3]

go deeper

for a junior

Know that extendleft exists and that the batch you pass lands in the opposite order. If you only remember one thing, remember to write reversed(...) when you prepend a batch.

for a middle

Derive the reversal from the definition — repeated appendleft calls, each displacing the last — rather than memorising it, and explain why a streaming iterable cannot be landed in order without buffering it.

for a senior

Frame it as a silent-correctness risk rather than trivia: it produces wrong ordering with no exception, single-item tests hide it, and the review habit is to make the intent explicit at the call site.

for a principal

Use it as an example of API design under a streaming constraint: the standard library chose predictable memory over intuitive ordering and documented the consequence, which is the trade you make in your own iterator-consuming APIs.

## Where the reversal comes from `collections.deque` gives you a mirrored pair of methods at each end: `append`/`appendleft`, `pop`/`popleft`, `extend`/`extendleft`. The mirroring is exact for the single-item ones, but `extendleft` has a consequence that catches people, and the documentation states it plainly: `extendleft(iterable)` is equivalent to ```python for x in iterable: d.appendleft(x) ``` Run that by hand on `deque([1, 2, 3])` with the iterable `[4, 5, 6]`. Push 4: the deque is `4, 1, 2, 3`. Push 5, which must also go leftmost, so it lands *in front of* 4: `5, 4, 1, 2, 3`. Push 6: `6, 5, 4, 1, 2, 3`. The added items appear in the opposite order to the input, because each one displaces the previous one from the front position. The last item consumed is the one nearest the left end. This is the only sane definition. `extendleft` accepts any iterable — a generator, a file object, an `itertools` chain — and an iterable can only be consumed forwards, one item at a time, with no known length. To land the items in input order at the left, the implementation would have to buffer the whole iterable first and then push it backwards, which would silently allocate an unbounded amount of memory for an unbounded generator. Streaming and order-preserving are incompatible here, and the standard library chose streaming. ## Getting the order you wanted Reverse the input, not the output: ```python d = deque([1, 2, 3]) d.extendleft(reversed([4, 5, 6])) # deque([4, 5, 6, 1, 2, 3]) ``` `reversed()` needs a sequence or an object implementing `__reversed__`; for a one-shot generator you have to materialise it first (`list(gen)`) and accept the memory that costs, which is a useful moment to ask whether you needed the left end at all. For a sliceable sequence, `items[::-1]` does the same job and is often clearer in a small expression. ## The neighbouring surprise: rotate `d.rotate(n)` rotates the deque **right** by n steps: each element moves n places towards the right end, and the items that fall off the right come back on the left. `d.rotate(1)` is exactly `d.appendleft(d.pop())`. A negative argument rotates left: `d.rotate(-1)` is `d.append(d.popleft())`. The sign convention trips people who expect a positive number to mean "forwards through the list". Rotation costs O(k) where k is `abs(n)` bounded by the length, not O(n) per step, and it makes cyclic work — round-robin ordering, aligning a fixed window — a one-liner instead of a slice-and-concatenate. ## Why the detail is worth knowing It is a small piece of API trivia, but it is the kind that produces a quiet data bug rather than a crash. A function that prepends a batch of newly-fetched items to a work deque with `extendleft` will process that batch backwards, and nothing raises. Tests written against a single-element batch pass. The habit to build is: whenever you push a *batch* at the left end, either write `reversed(...)` in the same expression or add a comment saying the reversal is intended — and remember that `extend` on the right has no such caveat, because arrival order and left-to-right order agree there. ## The same rule elsewhere in the API Once you have internalised "defined as repeated single pushes", the rest of the deque's bulk operations stop surprising you. `d.extend(it)` is `for x in it: d.append(x)`, which preserves order because each item lands to the right of the last. The constructor `deque(iterable)` behaves like `extend`, so `deque([1, 2, 3])` reads left to right as written — and, on a bounded deque, the same per-item eviction rule applies while the constructor consumes the iterable. Two neighbouring methods are worth keeping distinct in your head. `d.reverse()` reverses the deque **in place** and returns `None`, like `list.reverse`. `reversed(d)` returns an *iterator* over the items back to front and leaves the deque alone — that is the one you pass to `extendleft`. Mixing them up produces the classic `None` bug: `d = d.reverse()` throws away the deque. Finally, note what this trivia costs to get wrong. There is no exception, no warning, and no type error — only items in the opposite order. Write the test with at least two elements in the batch, because a one-item batch cannot distinguish the two orderings and will pass either way.

  • Why can't `extendleft` just preserve the input order internally?
    Because it accepts any iterable, including an infinite or single-pass generator. Landing the items left-to-right would require reading the whole iterable into memory first and then pushing it backwards, which turns a streaming operation into an unbounded allocation. The standard library kept it streaming and documented the reversal instead.
  • Which direction does `deque.rotate(1)` move the items?
    Right. `d.rotate(1)` is equivalent to `d.appendleft(d.pop())`: the rightmost element wraps around to the front. A negative argument rotates left, so `d.rotate(-1)` equals `d.append(d.popleft())`. The cost is proportional to the rotation distance, not to a per-step traversal of the whole deque.

It is like handing people to the front of a queue one at a time: whoever you hand over last is standing in front of everyone you handed over earlier.

saying these in an interview costs you the question

  • Expects extendleft to preserve input order
  • Calls the reversal a CPython bug
  • Says extend and extendleft differ only in end
  • Reaches for a loop of list.insert(0, x) instead
  • Thinks rotate with a positive n moves items left

context