skip to content

Where does the value go when a generator function executes `return total`?

level: juniorimportance: should knowfreq 26%

answer

  1. The last value is not an item
  2. Ordinary loops throw it away silently
  3. It rides out on the stop signal
  4. StopIteration carries a value attribute
  5. Bare return means that value is None

basics

~20 s

It is not yielded. return total ends the generator and the value is attached to the StopIteration that the generator raises, as exc.value. A for loop or list() discards it silently; you see it only by catching StopIteration yourself.

solid answer

~50 s

A generator's `return` does not produce an item — it terminates the generator, and Python packages the returned object into the `StopIteration` exception that signals exhaustion, where it is available as the exception's `value` attribute. Ordinary consumption throws that away: `for item in gen` and `list(gen)` treat `StopIteration` purely as the stop signal, so a generator that returns a summary looks to them exactly like one that returns nothing. To read it you must drive the generator yourself with `next()` or `send()` and catch `StopIteration as stop`, then use `stop.value`. A bare `return` or falling off the end gives `value` of `None`. This has been true since Python 3.3, which first allowed a value on `return` in a generator; and since 3.7 you cannot fake it by raising `StopIteration(total)` inside the body, because that becomes a `RuntimeError`.

code

python · 14 lines
python
def counted(items):
    n = 0
    for item in items:
        n += 1
        yield item
    return n

g = counted("abc")
while True:
    try:
        print(next(g))
    except StopIteration as stop:
        print("total:", stop.value)   # total: 3
        break

go deeper

for a junior

Recall that return in a generator ends it rather than producing an item, and that the returned object is carried on the StopIteration as its value attribute.

for a middle

Explain why ordinary consumption never shows it — for, list() and comprehensions treat StopIteration purely as the stop signal — and show the manual except StopIteration as stop pattern that reads stop.value.

for a senior

Point out the silent-loss hazard when someone writes a generator that returns a summary and every caller consumes it with a for loop, and be ready to argue for a clearer interface.

for a principal

Own the API guidance: a control exception is a poor carrier for a result, so decide when a stage should be a plain generator and when it should be a typed object exposing both the stream and its summary.

## `yield` produces items; `return` produces an ending Inside a generator function the two keywords do genuinely different jobs. `yield` hands an item to whoever is consuming the generator and suspends the frame. `return` ends the generator: the frame unwinds, any `finally` blocks run, and the generator is closed forever. There is no third channel — so what does Python do with the object on a `return`? It attaches it to the exception that already exists for signalling the end. When a generator finishes, it raises `StopIteration`, and that exception has a `value` attribute holding whatever was returned. A bare `return`, or simply running off the end of the body, produces a `value` of `None`. ```python def counted(items): n = 0 for item in items: n += 1 yield item return n # the total, not an item ``` ## Why you have probably never seen it Because every ordinary way of consuming a generator throws it away. A `for` loop, `list()`, `sum()`, a comprehension, `min()` — all of them treat `StopIteration` as a control signal meaning "no more items" and discard the exception object with everything on it. From their point of view a generator that `return`s a total is indistinguishable from one that returns nothing, and no warning is issued. This is a genuine trap: a function written to "return a count at the end" quietly produces nothing observable when someone consumes it with a `for` loop. To see the value you must drive the generator by hand and catch the exception: ```python g = counted("abc") while True: try: print(next(g)) except StopIteration as stop: print("total:", stop.value) break ``` The `as stop` binding is the whole trick: `stop.value` is the returned object. ## What you should do instead, most of the time If a caller needs both a stream and a summary, a return value carried on an exception is a poor interface: it forces every consumer to abandon `for` loops. Better options are to yield the summary as a final, clearly-typed item; to have the generator write the total into an object the caller owns; or to expose a small class with an iteration method plus a `total` attribute, so the two results are ordinary API rather than something smuggled out on a control exception. Reserve the return value for the case where the consumer really is machinery driving the generator step by step. ## The `StopIteration` traps around it **You cannot raise it yourself.** Writing `raise StopIteration(n)` inside a generator body to "return a value" does not work: since PEP 479 (Python 3.7) any `StopIteration` that escapes a generator body is converted into `RuntimeError: generator raised StopIteration`. That change exists because such an exception, escaping accidentally from a nested call, used to end the iteration silently and truncate results. The only supported way to end a generator with a value is `return`. **Empty is not the same as returning.** `StopIteration` with a `value` of `None` is what every plain generator raises when it ends; the attribute exists on every instance, so `stop.value` never raises `AttributeError` — it is simply `None` when there was nothing to return. **`return` still runs `finally`.** The return does not skip cleanup: `finally` blocks in the generator body execute as the frame unwinds, before the `StopIteration` reaches the caller. ## Version history in one line Before Python 3.3, a `return` with a value inside a generator was a `SyntaxError`; 3.3 allowed it and gave `StopIteration` its `value` attribute; 3.7 closed the accidental-`StopIteration` hole; and none of it has changed through 3.14.

  • What is `StopIteration.value` for a generator that just falls off the end of its body?
    `None`. The attribute always exists, so reading it is safe; it holds `None` for a bare `return` and for a body that simply finishes. Only an explicit `return <expr>` puts something else there.
  • Why does `raise StopIteration(total)` inside a generator body not work as a return?
    Since Python 3.7, a `StopIteration` that escapes a generator body is converted into `RuntimeError: generator raised StopIteration`. The rule exists because such an exception leaking out of a nested call used to truncate iteration silently. Use `return` instead.
  • If a `for` loop discards the returned value, how should a generator report a summary to ordinary callers?
    Make it part of the API rather than smuggling it on an exception: yield a final, clearly distinguishable summary item, write the total into an object the caller supplied, or return a small class that is iterable and also exposes the summary as an attribute.

saying these in an interview costs you the question

  • Thinks the returned value is yielded as a final item
  • Expects a for loop to expose the return value
  • Says return in a generator is a SyntaxError
  • Raises StopIteration with a value to return
  • Assumes StopIteration.value is missing when unused

context