yield from semantics
The gotcha
yield from gen looks like a shortcut for for x in gen: yield x. It’s not — it delegates the full generator protocol (send, throw, return). The naive for ... yield form silently breaks send() and discards the subgenerator’s return value.
Minimal repro
def sub():
x = yield 1
y = yield 2
return x + y # subgenerator's return value
# Naive delegation — broken
def parent_naive():
for v in sub():
yield v
# Correct delegation
def parent():
result = yield from sub()
print("sub returned:", result)
yield result
g = parent()
print(next(g)) # 1
print(g.send(10)) # 2 — sends 10 into sub's first yield
print(g.send(20)) # sub returned: 30 \n 30
With parent_naive, g.send(10) is just next(g) — the value is discarded because the for loop only consumes via __next__. The naive form is a one-way data flow.
What yield from actually does
def parent():
yield from sub()
is roughly equivalent to:
def parent():
_i = iter(sub())
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while True:
try:
_s = yield _y # forward sent value
except GeneratorExit:
_i.close(); raise # forward close
except BaseException as _e:
_i.throw(_e) # forward exception
else:
try:
_y = _i.send(_s) if _s is not None else next(_i)
except StopIteration as _e:
_r = _e.value # capture return value
break
# _r is what `yield from` evaluates to
It forwards next, send, throw, close to the subgenerator and captures the return value when the subgenerator ends.
Capturing the return value
def reader():
line = yield "ready?"
return f"got: {line}"
def driver():
result = yield from reader()
yield result
g = driver()
print(next(g)) # "ready?"
print(g.send("hello")) # "got: hello"
A generator’s return value doesn’t yield — it sets StopIteration.value. The yield from expression evaluates to that value, giving you a clean way to compose generators that produce a final result.
Composing generators
yield from is what made coroutines possible in pre-async Python:
def chunks(lines):
chunk = []
for line in lines:
chunk.append(line)
if len(chunk) == 100:
yield chunk
chunk = []
if chunk:
yield chunk
def process(source):
yield from chunks(source) # transparent delegation
Tree traversal becomes elegant:
def walk(node):
yield node.value
for child in node.children:
yield from walk(child)
Difference from async await
yield from was the prototype for await. Both delegate work to a sub-coroutine. In modern Python:
# Generator-based coroutine (legacy, pre-3.5)
@asyncio.coroutine
def fetch_old():
data = yield from request() # yield from
return data
# Native coroutine (3.5+)
async def fetch_new():
data = await request() # await replaces yield from for coroutines
For ordinary generators producing values, yield from is still the right tool. For async, use await.
Common pitfalls
- Forgetting
yield fromand usingfor x in sub(): yield x— works for read-only delegation, breaks send/throw. - Trying
yield fromon a non-iterable —TypeError.yield fromrequires an iterable; for a single value, justyield value. yield fromof a generator that already started — works, but you only get the rest, not from the beginning.
g = sub()
next(g) # consumes first yield
def parent():
yield from g # picks up where g left off
Interview angle
- Q: “What does
yield fromdo that aforloop doesn’t?” — full protocol delegation: forwardssend,throw,close, captures return value. - Q: “Why is
yield fromneeded for coroutines?” — bidirectional data flow; values can be sent into the subgenerator, return values bubble out. - Follow-up: “How do you capture a generator’s return value?” —
result = yield from gen()—return valuebecomesStopIteration.value, surfaced as the expression value. - Follow-up: “What replaced
yield fromfor async?” —await(PEP 492, 3.5+).
See 08_generator_exhaustion.md, 22_generators_iterators.md, 09_async_def_returns_coroutine.md.