skip to content

What happens when a Python generator function executes a return statement?

level: middleimportance: should knowfreq 46%

answer

  1. Termination is signalled, not returned
  2. The protocol uses an exception to stop
  3. A returned value is not an item
  4. It rides on the exception instead
  5. Escaping StopIteration became RuntimeError in 3.7

basics

~20 s

It ends the generator by raising StopIteration rather than yielding anything. Any returned value is not produced as an item; it is attached to that StopIteration. A for loop catches the exception and simply stops.

solid answer

~50 s

`return` in a generator terminates the body, and termination is signalled by raising `StopIteration` at the consumer's `next()` call. Falling off the end of the body does the same thing. A returned value is **not** yielded — since Python 3.3 a generator may write `return value`, and that value is carried on the exception as `StopIteration.value`, where only code that catches the exception can see it. A `for` loop, `list()` and every other consumer of the iterator protocol swallow `StopIteration` and treat it as "the sequence is over", so a returned value is invisible to them. One important related rule: since Python 3.7 (PEP 479), a `StopIteration` that escapes from a generator body — typically a bare `next()` on an exhausted iterator inside the body — is converted to `RuntimeError` instead of quietly ending the outer generator.

code

python · 12 lines
python
def sample():
    yield 1
    return "done"

print(list(sample()))          # the returned value is not an item

it = sample()
next(it)
try:
    next(it)
except StopIteration as exc:
    print("StopIteration.value =", exc.value)

go deeper

for a junior

Remember that return inside a generator stops it rather than producing a value, and that a for loop over it just ends quietly with nothing extra appearing in the output.

for a middle

Explain the mechanics: termination is signalled by StopIteration, a returned value rides on that exception rather than being yielded, and consumers of the iterator protocol swallow it.

for a senior

Show you know the PEP 479 boundary from 3.7 — an escaping StopIteration becomes RuntimeError — why that rule exists, the silent-truncation bugs it replaced, and the safe spellings for advancing an inner iterator inside a generator body.

for a principal

Own the API convention: an out-of-band return value that normal consumers cannot see is a poor contract for summaries or totals. Decide how producers in your codebase report completion state so callers are not forced to catch exceptions to read results.

### Two ways a generator body ends A generator's body finishes either by running off the end or by executing `return`. Both produce the same signal to the consumer: the pending `next()` raises `StopIteration`. There is no "final value" slot in the iterator protocol, so termination has to be an exception rather than a sentinel value — any sentinel could collide with a legitimate item. After that, the generator is finished for good. Further `next()` calls raise `StopIteration` again immediately, `gi_frame` is `None`, and `inspect.getgeneratorstate()` reports `GEN_CLOSED`. ### return with a value Python 3.3 made `return value` legal inside a generator (before that it was a `SyntaxError`). The value is **not** yielded — consumers iterating normally never see it. It is stored on the raised exception: ```python def sample(): yield 1 return "done" ``` Iterating that with a `for` loop produces exactly one item, `1`, and the string is discarded along with the swallowed `StopIteration`. To observe it you must catch the exception yourself and read `StopIteration.value` — which is why the returned value is best thought of as an out-of-band result for a delegating caller, not as data for a normal loop. This is the single most common misread in interviews: candidates expect `return "done"` to append `"done"` to the output, by analogy with an ordinary function. It does not appear in `list(sample())` at all. ### Why for loops look like they ignore it `for x in gen:` is defined as: call `iter()`, then call `__next__()` repeatedly, and when `StopIteration` is raised, exit the loop normally. The exception is part of the protocol, not an error condition — which is why nothing is printed, nothing propagates, and a `finally` in the loop still runs. `list()`, `sum()`, tuple unpacking and comprehensions all behave identically because they all drive the same protocol. ### A bare return is a clean early exit Inside a generator, `return` with no value is the idiomatic way to stop producing items early — the equivalent of `break` in the consumer, but decided by the producer: ```python def until_blank(lines): for line in lines: if not line.strip(): return # stop the generator; equivalent to falling off the end yield line ``` It raises `StopIteration` with a value of `None`, and the consumer's loop simply ends. ### PEP 479: StopIteration must not leak Before Python 3.7, a `StopIteration` raised *inside* a generator body by something other than the generator's own completion would propagate and be mistaken for normal termination — so a generator that called `next(inner)` on an exhausted iterator ended silently and truncated its own output, with no error anywhere. Data-loss bugs of that shape were hard to find. PEP 479 changed it: a `StopIteration` that escapes a generator body is replaced by a `RuntimeError` (chained to the original). Opt-in via `from __future__ import generator_stop` in 3.5–3.6, mandatory from 3.7 onward, including 3.14. ```python def leaky(src): while True: yield next(src) # raises RuntimeError once src is exhausted ``` The correct spellings are to iterate with a `for` loop, to pass a default (`next(src, None)`) and check it, or to catch `StopIteration` locally and `return`. Knowing this rule is what separates "return raises StopIteration" as a memorized line from actually understanding the boundary: the generator's *own* completion raises `StopIteration` outward, but any `StopIteration` arriving from within its body is treated as a bug. ### Practical implications * **Do not return data you expect the loop to see.** If a caller needs a summary, yield it as a final item with a distinguishable shape, or expose it as an attribute on an object, rather than relying on the return value. * **Do not raise StopIteration to end a generator.** Use `return`. Raising it explicitly inside the body now triggers the `RuntimeError` conversion. * **Cleanup still runs.** `finally` blocks and `with` bodies in the generator execute as the body unwinds through `return`, which is where resource release belongs. * **Exhaustion is permanent.** Once `StopIteration` has been raised, the generator cannot be restarted; a fresh call to the generator function is the only way to iterate again. ### Checking termination in practice Two quick observations settle most confusion about where a generator ended up. `inspect.getgeneratorstate()` returns `GEN_CLOSED` for a generator whose body has returned, and `gi_frame` is `None` at that point — so "did it finish, or is it parked at a yield?" is directly answerable rather than a guess. And because `StopIteration` is an ordinary exception class, you can always drive the generator with `next()` inside a `try` and inspect what came back, which is the only way to see a returned value without a delegating construct. The overall shape to hold on to: `yield` produces items, `return` produces termination, and the two travel by different channels. Mixing them up — expecting a return to appear as an item, or raising `StopIteration` where a `return` belongs — accounts for most of the surprises in this corner of the language.

  • Why does list() on a generator that returns a value never contain that value?
    Because `list()` drives the iterator protocol: it calls `__next__()` until `StopIteration` is raised, then stops and discards the exception. The returned value lives on that exception object, and nothing in the protocol copies it into the sequence. Only code that catches `StopIteration` itself — or a delegating construct designed to read it — ever observes the value.
  • What is wrong with calling next(inner) directly inside a generator body?
    When `inner` is exhausted, `next()` raises `StopIteration` inside your body. Since Python 3.7 that escaping exception is converted to `RuntimeError`, so the generator fails loudly instead of ending silently as it did pre-3.7. Iterate `inner` with a `for` loop, use `next(inner, default)` and test the default, or catch `StopIteration` locally and `return`.
  • Do finally blocks in a generator run when the body returns?
    Yes. `return` unwinds the body normally, so `finally` blocks and `with` cleanups execute before `StopIteration` reaches the consumer. That is what makes a `with` inside a generator body the right place for resources, as opposed to opening them and relying on the frame being collected later.

saying these in an interview costs you the question

  • Says the returned value becomes the last yielded item
  • Thinks return in a generator is a SyntaxError
  • Raises StopIteration explicitly to end a generator
  • Expects a for loop to surface the return value
  • Believes an exhausted generator restarts on the next call
  • Unaware that escaping StopIteration becomes RuntimeError

context