Where does a Python generator's return value go? StopIteration.value and PEP 479
A generator's return value never shows up in a for loop; it rides on StopIteration.value. How yield from gets it, why StopIteration in a generator becomes RuntimeError, and what close() returns.
You can write return value inside a generator function. It is not a syntax error and Python does not warn about it. Run that generator through a for loop or list(), though, and the value never appears. It hasn’t vanished. It travels inside the StopIteration exception that signals the end of iteration.
return raises StopIteration
The language reference puts it this way: in a generator function, return indicates that the generator is done and causes StopIteration to be raised. If a value is returned, it is used to construct the StopIteration and becomes its value attribute. With nothing passed, value is None.
def read_items():
yield "a"
yield "b"
return 2 # number of items read
gen = read_items()
print(next(gen)) # a
print(next(gen)) # b
try:
next(gen)
except StopIteration as e:
print(e.value) # 2
To see the value you have to call next() yourself and catch StopIteration. The reference for the for statement only says that each item from the iterator is assigned to the target, and that when the iterator is exhausted the else clause runs and the loop ends. There is no syntax for reading the value carried by that last StopIteration. This loop handles "a" and "b" and drops the 2:
for item in read_items():
print(item)
Return values arrived in Python 3.3. According to the built-in exceptions page, 3.3 added the value attribute together with the ability for generator functions to use it to return a value.
yield from is the receiving end
The natural way to receive the value is to delegate from another generator with yield from. The reference says that when the subiterator finishes, the value of the raised StopIteration becomes the value of the yield from expression, and that when the subiterator is a generator this is filled in automatically from its return.
def outer():
count = yield from read_items()
yield f"read {count} items"
print(list(outer())) # ['a', 'b', 'read 2 items']
yield from does more than catch the value. Items from the subiterator go straight to whoever is calling the outer generator, and values passed with send() and exceptions passed with throw() are forwarded to the subiterator if it has those methods. Rewrite it as a loop that re-yields each item and you lose the forwarding and the return value together.
StopIteration inside a generator becomes RuntimeError
Separately from return values, handling StopIteration inside a generator has a sharp edge. The built-in exceptions page states that if generator code directly or indirectly raises StopIteration, it is converted into a RuntimeError, with the original StopIteration kept as the new exception’s cause (__cause__). That is PEP 479.
The indirect case is the common one. Calling next() on another iterator without a default raises StopIteration when that iterator is empty.
def pairs(it):
it = iter(it)
while True:
first = next(it)
second = next(it)
yield first, second
list(pairs([1, 2, 3, 4]))
Before PEP 479, a StopIteration escaping from next(it) looked, from outside the generator, exactly like a normal end of iteration. So this code stopped quietly when the input ran out, and a StopIteration caused by a bug would have stopped it just as quietly. Under the current rule, the moment the input runs dry you get a RuntimeError. An even-length input doesn’t avoid it: the fifth next(it) still raises StopIteration.
If you mean to stop, stop with return:
_END = object()
def pairs(it):
it = iter(it)
while True:
first = next(it, _END)
if first is _END:
return
second = next(it, _END)
if second is _END:
return # drop a trailing unpaired item
yield first, second
Given a default, next() returns it instead of raising StopIteration when the iterator is exhausted. If None can legitimately appear in the input, a dedicated sentinel object is safer than None. Catching StopIteration in a try and then returning works too. What matters is that no StopIteration leaks out of the generator. For the same reason, old code that does raise StopIteration(value) inside a generator should become return value.
The conversion only applies to generators (and coroutines). A class-based iterator’s __next__() still has to raise StopIteration to say it is finished.
| Version | Behaviour |
|---|---|
| 3.5 | Converted only in modules with from __future__ import generator_stop |
| 3.7 | Converted in all code by default (listed as mandatory in the __future__ table) |
The What’s New in Python 3.7 page says the same: PEP 479 is enabled for all code, so StopIteration raised directly or indirectly in coroutines and generators is turned into RuntimeError. Code written for 3.6 or earlier can change behaviour when moved across that line.
close() returns a value too (since 3.13)
close() raises GeneratorExit at the point where the generator is suspended. If the generator catches it and returns a value, Python 3.13 and later hand that value back as the result of close(). Earlier versions returned None.
def summer():
total = 0
try:
while True:
total += yield
except GeneratorExit:
return total
acc = summer()
next(acc) # run to the first yield
acc.send(3)
acc.send(4)
print(acc.close()) # 3.13+: 7, earlier: None
The initial next(acc) is there because when send() starts a generator there is no yield expression yet to receive the value, so the argument must be None. The close() documentation covers a few more cases. If the generator has already finished, or lets GeneratorExit propagate without catching it, close() returns None. If it yields again after receiving GeneratorExit, a RuntimeError is raised. Any other exception propagates to the caller.
Async generators can’t return a value
In an asynchronous generator, an async def that contains yield, a return with a value is a syntax error. Only a bare return is allowed, and it raises StopAsyncIteration. A design that relies on a sync generator’s return value has to change when it goes async: emit the result as the last yield, or put it in a separate object.
In short
- A generator’s
return valuelands inStopIteration.value.forandlist()never show it. - To get it, delegate with
yield from, or callnext()and catchStopIterationyourself. - Since 3.7, a
StopIterationleaking out of a generator becomesRuntimeError. End withreturn, and givenext()on other iterators a default or catch the exception. - Since 3.13,
close()returns whatever the generator returned while handlingGeneratorExit.
For another case of an exception slipping out of sight, see Java’s try-with-resources and suppressed exceptions. For a different Python surprise tied to when code runs, see why [] is a bad default argument.
The behaviour described here was checked against the Python 3.15.0 documentation on 18 October 2026. The outputs shown in the examples follow from the documented rules; they were not produced by running the code for this article.