What does a Python for loop do under the hood with iter() and next()?
answer
- Sugar over two builtin calls
- One call up front, many after
- An exception ends it, not a length check
- iter() then next() until StopIteration
basics
~20 sA for loop calls iter() on the object once to get an iterator, then calls next() on that iterator repeatedly, binding each result to the loop variable. When next() raises StopIteration, the loop catches it and ends normally.
solid answer
~30 s`for value in data:` compiles to: call `iter(data)` once, then loop calling `next()` on the returned iterator until it raises `StopIteration`, which the loop catches and treats as a clean end. Written out by hand it is `it = iter(data)` followed by `while True:` with `try: value = next(it)` / `except StopIteration: break`. `iter()` dispatches to `type(data).__iter__`, `next()` to `type(it).__next__`. `StopIteration` is control flow rather than an error: it never reaches your code and never appears in logs. Note the loop only catches it from its own `next()` call — a `StopIteration` raised in the loop *body* propagates out normally.
code
python · 13 linesdata = [10, 20, 30]
for value in data:
print(value)
# exactly what the statement above does
it = iter(data)
while True:
try:
value = next(it)
except StopIteration:
break
print(value)go deeper
Be ready to recite the three steps in order: iter() once, next() repeatedly, StopIteration ends it. Then write the while True / try / except StopIteration: break form on a whiteboard without hesitating.
Explain the dispatch: iter(x) goes to type(x).__iter__, next(it) to type(it).__next__, and the two distinct TypeError messages tell you which half is broken. Know that iter() runs exactly once per loop.
Show where you drive the protocol by hand — peeking at a first record, merging ordered streams, resuming a partly-consumed iterator — and why an except Exception around a manual next() call silently turns exhaustion into a no-op.
Own the design consequence: because exhaustion is a caught exception rather than a return value, a truncated stream and a complete one look identical to the consumer. Decide where completeness is asserted explicitly instead of inferred from a loop ending.
## The whole protocol in one sentence A `for` statement is syntax sugar over two builtin calls and one exception. Executing `for value in data:` evaluates `data`, calls `iter(data)` **exactly once** to get an iterator, then repeatedly calls `next()` on that iterator, binding each returned object to the loop variable and running the body. When the iterator signals exhaustion by raising `StopIteration`, the loop machinery catches that one exception, throws it away, and execution continues after the loop. Nothing else about the object is consulted: not its length, not its class name, not whether it is a list. ## The hand-desugared form ```python it = iter(data) while True: try: value = next(it) except StopIteration: break ... # the loop body ``` Interviewers ask for this rewrite because it forces you to name all three moving parts in order: the one-time `iter()` call, the repeated `next()` call, and `StopIteration` as the terminator. Getting the order wrong, or calling `next()` on `data` instead of on the object `iter()` returned, is the usual tell. ## `iter()` and `next()` are thin dispatchers `iter(x)` invokes `type(x).__iter__(x)` and returns whatever comes back; that return value is what the loop drives. `next(it)` invokes `type(it).__next__(it)` and returns the value it produces. If `type(x)` supplies no way to iterate, `iter(x)` raises `TypeError: 'X' object is not iterable`. If the object handed back does not supply `__next__`, `next()` raises `TypeError: 'X' object is not an iterator` — a different message, and worth learning apart, because it points at a different bug: something returned the wrong thing from `__iter__`. At the C level the loop does not even go through the builtin functions. The compiler emits a `GET_ITER` instruction followed by a `FOR_ITER` instruction; `GET_ITER` performs the `iter()` call once, and `FOR_ITER` calls the iterator's next slot on each pass and jumps past the loop when that slot signals exhaustion. The semantics are identical to the desugared Python above, which is why the desugaring is the right mental model even though no `try` block literally exists in the bytecode. ## `StopIteration` is control flow, not a failure `StopIteration` is an ordinary `Exception` subclass, not a `BaseException`-only signal, so a careless `except Exception:` wrapped around a manual `next()` call will swallow it and turn "the stream ended" into "we did nothing and moved on". Inside a `for` loop, though, it is the loop's own private signal: it never surfaces to your code, never reaches a logger, and never sets a non-zero exit status. A loop that ends because of it looks exactly like a loop that ends because the data ran out, which is normally what you want and occasionally what hides a bug. One boundary matters: the loop catches `StopIteration` only from its **own** call on the iterator. If the loop *body* raises `StopIteration` — typically because it called `next()` on some other, already-exhausted iterator — that exception propagates out of the `for` statement like any other exception rather than quietly ending the loop. ## Why `iter()` runs once A common wrong answer is that the loop calls `iter()` before each item. It does not; the iterator produced at loop entry is held for the duration. That single call is what makes the protocol composable: you can call `iter()` yourself, consume a few items with `next()`, and then hand the *same partly-consumed* iterator to a `for` loop, which resumes where you left off. That is how a header row is read off a stream before looping over the remaining records, and it is the reason the manual form is worth knowing rather than merely reciting. ## Driving the protocol by hand Manual `iter()`/`next()` shows up whenever a loop is not the right shape: peeking at the first element to decide how to parse the rest, merging two ordered streams by advancing whichever side is behind, or implementing a wrapper that consumes items on demand. In each case you write the `try`/`except StopIteration` yourself, and you decide what exhaustion means — end the merge, fall back to a default, or raise a domain error. ## Naming across versions `__next__` is the Python 3 spelling of the method; Python 2 called it `next()`, and the builtin `next()` function was added so code could be written against both. On any Python 3 release, including 3.14, the protocol is `__iter__` plus `__next__` plus `StopIteration`, and it has not changed.
- How many times does a for loop call iter() on the object it is looping over?Once, at loop entry. The compiler emits a `GET_ITER` instruction before the loop body and keeps the resulting iterator for the whole loop; only `next()` runs per iteration. That is why you can call `next()` yourself first and then hand the same iterator to a `for` loop — it picks up where you stopped, instead of restarting.
- If code inside the loop body raises StopIteration, does the loop just end?No. The loop catches `StopIteration` only from its own `next()` call on the iterator it is driving. A `StopIteration` raised in the body — usually from calling `next()` on some other, exhausted iterator — propagates out of the `for` statement like any other exception. Inside a generator function it is converted into a `RuntimeError` instead of escaping.
- What error do you get looping over an object whose type provides no way to iterate?`iter()` raises `TypeError: 'X' object is not iterable`. A different message, `TypeError: 'X' object is not an iterator`, comes from `next()` and means something handed back an object without `__next__` — usually an `__iter__` that returned the wrong thing. Reading which of the two messages you got points straight at the broken half.
saying these in an interview costs you the question
- Says the for loop calls __next__ directly on the container
- Treats StopIteration as an error the loop failed to handle
- Claims iter() is called again on every iteration
- Thinks for loops work by indexing from 0 to len()
- Uses __iter__ and __next__ as if they were the same method