skip to content

Why does reversed() raise TypeError on a generator object but work on a list?

level: middleimportance: should knowfreq 42%

answer

  1. Backwards needs a known end
  2. Iterable is not enough
  3. A dunder, or length plus indexing
  4. One-pass streams are refused on purpose
  5. Buffer it, and pay for the buffer

basics

~20 s

reversed() must walk an object backwards, so it needs either a reversed method or the sequence protocol: len plus integer getitem. A generator object offers neither and can only move forward, so reversed() raises TypeError.

solid answer

~50 s

`reversed(obj)` first looks for `type(obj).__reversed__` and calls it if present. Failing that, it falls back to the sequence protocol: the object must report a length via `__len__` and accept integer indices via `__getitem__`, so `reversed` can count down from `len(obj) - 1` to `0`. Lists, tuples, `str`, `bytes` and `range` satisfy that, and `dict` and its views have supplied `__reversed__` since Python 3.8. A generator object, a `zip` object, a `map` object or an open file has no length and no indexing — it is a one-pass forward stream — so `reversed()` raises `TypeError`. Sets fail too, having no defined order to reverse. The only way to reverse a lazy stream is to buffer it: `reversed(list(gen))`, or `collections.deque(gen)` if you need bounded buffering. `reversed()` itself does not copy; it returns a lazy iterator over the existing object.

code

python · 12 lines
python
def stream():
    yield 1
    yield 2

try:
    reversed(stream())
except TypeError as exc:
    print("refused:", exc)

print(list(reversed([1, 2, 3])))
print(list(reversed(range(3))))
print(list(reversed({"a": 1, "b": 2})))

go deeper

for a junior

Recall that reversed() works on lists, tuples, strings and range, and that it returns an iterator you usually wrap in list() or feed to a for loop. Know that list.reverse() mutates and returns None.

for a middle

Explain the protocol: __reversed__ first, otherwise __len__ plus integer __getitem__, otherwise TypeError at call time. Contrast iterable with sequence and give the buffering workaround for a generator.

for a senior

Judge the memory cost before materializing a stream to reverse it, reach for bounded buffering when only the tail is needed, and prefer changing the producer's order over reversing large data after the fact.

for a principal

Argue about where ordering belongs in a pipeline: reversal after the fact forces buffering and caps throughput, so ordering guarantees are better specified at the boundary that produces the data than imposed downstream.

## Why the requirement exists Reversal is not something you can do to a stream. To yield the last element first you must already know where the end is, which means either the object can tell you its size and let you index into it, or the object knows how to produce its own elements backwards. `reversed()` therefore refuses anything that only supports forward iteration, and that refusal is deliberate rather than an oversight: silently buffering an arbitrarily large input would hide an unbounded memory cost behind a one-word call. ## The two routes reversed() accepts When you call `reversed(obj)`, the built-in first looks for `__reversed__` on the object's *type*. If it exists, it is called and its result — expected to be an iterator — is returned. Types that can produce their elements in reverse cheaply supply this: `range` does, and reversing a `range` is O(1) and allocates nothing, because the reverse of an arithmetic sequence is just another arithmetic sequence. If there is no `__reversed__`, `reversed()` falls back to the *sequence protocol*: the type must define `__len__` and `__getitem__` accepting integers. `reversed()` then hands back an iterator that walks the index from `len(obj) - 1` down to `0`, calling `__getitem__` at each step. Lists, tuples, `str` and `bytes` are handled this way. If neither route is available, the call raises `TypeError` immediately — at call time, not at first iteration. That distinction matters when debugging: the traceback points at the `reversed(...)` expression, not at the loop. ## What fails, and why Generator objects, `map` and `zip` objects, `filter` objects, `enumerate` objects, open file objects and most iterators returned by library code all fail. They implement `__iter__` and `__next__` and nothing else. Being *iterable* is not enough; that is the single sentence to say in an interview, because the misconception "`reversed` works on any iterable" is extremely common. Sets fail for a different reason. A `set` has a length, but it has no `__getitem__` and no meaningful order, so reversing it is not defined. `frozenset` is the same. Dictionaries used to fail, but since Python 3.8 `dict` and the `dict.keys()`, `dict.values()` and `dict.items()` views implement `__reversed__`, walking insertion order backwards — which is well defined because dictionaries have preserved insertion order since 3.7. ## Reversing something that cannot be reversed The honest options are all forms of buffering, and choosing between them is the interesting part. `reversed(list(source))` materializes everything, which is fine for a bounded input and catastrophic for a large or unbounded one. `collections.deque(source, maxlen=n)` buffers only the trailing `n` items and can then be reversed, which is the right tool when you only need the tail. For a file, reading it line by line and reversing means holding every line; if the file is large, seeking backwards in blocks and splitting on newlines yourself is the standard alternative. And frequently the real answer is to reconsider the pipeline: produce the data in the order you need it — sort descending at the source, or push the reversal into whatever generated the stream — rather than reversing it after the fact. ## reversed() versus slicing versus list.reverse() Three operations are routinely confused. `reversed(xs)` returns a lazy iterator; nothing is copied and the original is untouched. It is the cheapest of the three, and because it is lazy it composes well with `next()`, `itertools` and early `break`. It is also a *view*: mutating `xs` while the iterator is live gives undefined-looking results. `xs[::-1]` builds a new reversed list (or string, or tuple) immediately. It costs a full copy but yields a real sequence you can index and reuse. On a `str` it is the idiomatic reversal, since strings are immutable. `xs.reverse()` mutates the list in place and returns `None`. The `None` return is the classic trap: `xs = xs.reverse()` silently rebinds the name to `None`, and the failure appears at the next use of `xs`. Pick `reversed()` when you are iterating once, slicing when you need a reusable reversed copy, and `list.reverse()` when in-place mutation is genuinely wanted and nothing else holds a reference expecting the old order. ## The interview shape Asked why `reversed()` failed, a strong answer names the protocol requirement, contrasts iterable with sequence, gives the buffering workaround with its memory cost, and notes that the cost is exactly why the built-in refuses rather than buffering silently.

  • What is the difference between reversed(xs), xs[::-1] and xs.reverse() for a list?
    `reversed(xs)` returns a lazy iterator over the existing list and copies nothing. `xs[::-1]` builds a new reversed list, costing a full copy but giving a reusable sequence. `xs.reverse()` mutates in place and returns `None` — assigning its result to a name is a classic bug that replaces the list with `None`.
  • Why does reversed() refuse a set even though a set has a length?
    A set has `__len__` but no `__getitem__` and no `__reversed__`, and more fundamentally no defined order to reverse. Membership order in a set is an implementation detail of hashing, so there is no last element to start from. Sort it into a list first if you need a deterministic backwards walk.
  • How would you reverse a stream too large to hold in memory?
    You cannot do it lazily — the last element must be found before the first is emitted. Either buffer the part you need with `collections.deque(source, maxlen=n)` for the tail, read the underlying file backwards in blocks, or push the ordering upstream so the data arrives in the order you want. Full materialization is the fallback, with its memory cost stated.

You can rewind a tape you already recorded, but not a live broadcast: to play a stream backwards you must first record all of it, and reversed() refuses to do that recording behind your back.

saying these in an interview costs you the question

  • Says reversed() works on any iterable
  • Claims reversed() returns a list
  • Thinks reversed() reverses the object in place
  • Says sets can be reversed because they have a length
  • Treats xs[::-1] and reversed(xs) as identical in cost
  • Assigns the result of list.reverse() to a name

context