Why can Python iterate a class that defines __getitem__ but no __iter__?
answer
- There were two iteration protocols
- iter() has a fallback path
- Counting up from zero
- It stops on IndexError
- The Iterable ABC disagrees
basics
~20 siter() falls back to the old sequence protocol: it hands back a built-in iterator that calls getitem with 0, 1, 2 and so on until the object raises IndexError. No iter is needed for a for loop to work.
solid answer
~40 sWhen `iter(obj)` finds no `__iter__`, CPython falls back to the pre-2.2 sequence protocol: it returns a built-in `iterator` object that calls `obj.__getitem__(0)`, then `(1)`, `(2)` … and stops when a call raises `IndexError` (or `StopIteration`). `for` loops, comprehensions, `list()`, unpacking and the `in` operator all funnel through `iter()`, so they all work. Two traps follow. First, the type check lies: `isinstance(obj, collections.abc.Iterable)` is `False`, because that ABC's hook only looks for `__iter__` — test by calling `iter(obj)` in a `try` if you must test at all. Second, the fallback is happy to misbehave: a `__getitem__` that takes string keys raises `KeyError: 0` on the very first step, and one that never raises `IndexError` iterates forever. Define `__iter__` on new code, and set `__iter__ = None` to opt out entirely.
code
python · 12 linesimport collections.abc
class Countdown:
def __getitem__(self, index):
if index > 4:
raise IndexError(index)
return index * 10
c = Countdown()
print(list(c))
print(30 in c)
print(isinstance(c, collections.abc.Iterable))go deeper
Know the observable fact: a class with only getitem can still be used in a for loop, because iter() falls back to asking for index 0, 1, 2 until an IndexError arrives.
Explain the mechanics — iter() tries iter, then the legacy sequence path that wraps the object in a built-in iterator — and name IndexError as the terminator. Knowing the collections.abc.Iterable mismatch is what separates you here.
Bring the failure modes: accidental iteration of a key-addressed object surfacing as KeyError: 0, unbounded iteration when IndexError never comes, and why isinstance against the Iterable ABC is the wrong gate. Say when you would set iter = None.
Frame it as a compatibility surface you inherit: a decades-old fallback that makes objects iterable by accident. Decide the house rule — always define iter explicitly, opt out with None where iteration is meaningless — and how you enforce it in review.
## Two iteration protocols, one function Python has had two ways to iterate. The modern one, introduced by PEP 234 in Python 2.2, is the iterator protocol: `iter(obj)` calls `type(obj).__iter__`, which returns an iterator whose `__next__` yields values and raises `StopIteration` when finished. The older one predates it: any object supporting integer subscription was iterable by counting up from zero. When PEP 234 landed, that older behaviour was kept as a fallback so existing sequence classes would keep working, and it is still there in Python 3.14 with no removal scheduled. So `iter(obj)` does three things in order: 1. If the type defines `__iter__`, call it and return the result (which must itself be an iterator). 2. Otherwise, if the type supports integer subscription, return a built-in `iterator` object that wraps the container and holds an internal counter starting at 0. 3. Otherwise raise `TypeError: 'X' object is not iterable`. The object produced by step 2 is a plain C-level iterator — `type(iter(obj))` prints `<class 'iterator'>`, not something defined in your module. Each `next()` on it calls `obj.__getitem__(i)` with the current counter and then increments it. ## What stops the loop, and what escapes The fallback iterator swallows exactly two exceptions and turns them into a clean end of iteration: `IndexError` and `StopIteration`. Every other exception propagates to the caller of `next()`, which in a `for` loop means it propagates out of the loop. That single rule explains the classic failure. A container whose `__getitem__` takes string keys — a config wrapper, a record, a row object — is *accidentally* iterable, because subscription support is all step 2 checks. Iterating it calls `__getitem__(0)`, the underlying dict lookup fails, and the loop dies with `KeyError: 0` rather than a sensible "this is not iterable". The mirror-image bug is a `__getitem__` that computes a value for every integer and never raises `IndexError`: `list(obj)` then never terminates and the process grows until it is killed. ## Everything funnels through `iter()` The fallback is not special-cased to `for`. Comprehensions, generator expressions, `list()`, `tuple()`, `sorted()`, `min()`, `max()`, `sum()`, star-unpacking and the `in` operator all call `iter()` under the hood, so all of them work on a `__getitem__`-only class. Membership testing is worth spelling out because it has its own ladder: `x in obj` tries `__contains__` first; with no `__contains__` it iterates the object and compares each item — identity first, then `==` — and that iteration itself goes through `iter()`, and so may end up on the `__getitem__` fallback. Define `__contains__` when a linear scan gives the wrong answer or is too slow for the container you are wrapping. ## The check that disagrees with reality The common interview twist: a class with only `__getitem__` is iterable, yet `isinstance(obj, collections.abc.Iterable)` returns `False`. There is no contradiction — that ABC's structural hook tests for `__iter__` and nothing else, and it has never tested for the fallback in any 3.x release. This is the strongest argument against `isinstance(x, collections.abc.Iterable)` as a gate: it can reject an object that iterates perfectly. If you genuinely must test, call `iter(x)` inside a `try` and catch `TypeError`; that asks the actual question. ## Opting out, and what to write in new code Setting a special method to `None` is the documented way to say an operation is unavailable. `__iter__ = None` on a class makes `iter()` raise `TypeError` *without* trying the `__getitem__` fallback, which is exactly what you want for a key-addressed object that should never be iterated by position. Alternatively, define a real `__iter__` that iterates the keys — that is what a mapping-shaped container should do. For new code the advice is simple: write `__iter__`. It is one line if you are wrapping something (`return iter(self._items)`), it makes the class pass every `Iterable` check, it lets you iterate something that is not integer-indexable, and it does not depend on a compatibility path that exists mainly for code written before Python 3. Understand the fallback because you will meet it in old code and in interviews; do not design around it. ## Each `iter()` call gets a fresh counter One underrated property of the fallback: the counter lives in the iterator object, not in your container, so every `iter(obj)` produces an independent walk. Nested loops over the same object therefore work — the inner loop restarts from 0 while the outer one keeps its own position — and so do two `zip`-ed traversals of the same instance. That is not automatic when you write `__iter__` yourself: the classic bug is a class whose `__iter__` returns `self` and whose `__next__` advances a counter stored on the instance, which makes the object single-pass and silently breaks nested iteration. If you replace an accidental fallback with a hand-written `__iter__`, return a *new* iterator each time rather than `self`, and you keep the behaviour callers already relied on. The per-step cost is a real `__getitem__` call into Python code, so the fallback is slower than a purpose-built `__iter__` over an underlying list — but that is a footnote, not the reason to avoid it. The reason to avoid it is that it makes iterability implicit, and implicit iterability is what produces `KeyError: 0` in somebody else's stack trace.
- Which exceptions end the `__getitem__` iteration fallback, and which escape it?`IndexError` and `StopIteration` are caught and turned into a normal end of iteration. Everything else propagates. That is why a `__getitem__` backed by a dict of string keys dies with `KeyError: 0` on the first step instead of reporting that the object is not iterable, and why a `__getitem__` that never raises `IndexError` makes `list(obj)` run until memory is gone.
- How do you make `iter()` refuse a class that still needs `__getitem__` for key access?Set `__iter__ = None` in the class body. Setting a special method to `None` is the documented way to mark an operation unavailable, and `iter()` then raises `TypeError: 'X' object is not iterable` without falling back to `__getitem__`. Subscription keeps working. The alternative is to define a real `__iter__` that yields the keys, which is what a mapping-shaped container should normally do.
- Does `x in obj` also reach the `__getitem__` fallback?Yes. The `in` operator tries `__contains__` first; without one it iterates the object and compares each item using identity then `==`, and that iteration goes through `iter()`, so a `__getitem__`-only class supports `in` too. The result of `__contains__` is coerced to a bool, so returning a truthy non-bool still yields `True`. Define `__contains__` when a linear scan is too slow or semantically wrong.
It is a legacy adapter left in the wall: the modern plug is __iter__, but the old socket still accepts anything that answers to numbered slots, one at a time, until it says there is no slot 5.
saying these in an interview costs you the question
- Saying a for loop calls __next__ on the container itself
- Claiming isinstance against collections.abc.Iterable proves iterability
- Assuming the fallback also stops on KeyError or TypeError
- Thinking the fallback passes real keys rather than 0, 1, 2
- Believing the __getitem__ fallback was removed in Python 3
- Saying only for loops use it, not in, list() or unpacking