What is `yield from`, and how is it used for generator delegation?
Quick Answer
yield from subgen delegates iteration to another generator/iterable. It yields every value subgen produces (as if written as a loop yielding each one), and also forwards .send()/.throw()/.close() calls through to subgen, evaluating to subgen's final return value. It's mainly used to flatten nested generators/compose generator pipelines without writing an explicit inner loop.
Detailed Answer
The basic simplification
def inner():
yield 1
yield 2
def outer_manual():
for value in inner(): # manually re-yield each value
yield value
def outer_yield_from():
yield from inner() # equivalent, more concise
list(outer_manual()) # [1, 2]
list(outer_yield_from()) # [1, 2]
For simple pass-through delegation, yield from inner() is just a more
concise form of looping and re-yielding. But the equivalence goes deeper
than syntax sugar for a loop.
Flattening nested structures
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # recursively delegate to sub-generator
else:
yield item
list(flatten([1, [2, 3, [4, 5]], 6])) # [1, 2, 3, 4, 5, 6]
yield from makes recursive generator flattening natural. Each
recursive call's yielded values bubble straight up through every level of
delegation without needing an explicit loop at each level.
Forwarding .send(), .throw(), and .close() (why it's more than a loop)
def inner():
received = yield "ready"
print(f"inner got: {received}")
yield "done"
def outer():
result = yield from inner() # forwards .send() values into inner() too
print(f"inner returned: {result}")
g = outer()
next(g) # 'ready'
g.send("hello") # prints "inner got: hello", yields 'done'
A plain for value in inner(): yield value loop would only forward
yielded values, not values sent back in via .send(). Those would go
to the for loop's implicit next() call, not to inner(). yield from
transparently forwards send/throw/close in both directions, which is
essential for coroutine-style generators (largely superseded by
async/await today, but still the mechanism asyncio was originally
built on).
Capturing the sub-generator's return value
def inner():
yield 1
yield 2
return "sub-generator done"
def outer():
result = yield from inner()
print(result) # 'sub-generator done'
list(outer()) # prints "sub-generator done", yields [1, 2]
yield from is itself an expression that evaluates to whatever the
delegated generator returned. A plain loop has no equivalent way to
capture that value.
yield from delegates iteration (and, less
commonly used today, send/throw/close forwarding) to a sub-generator
or iterable, doubling as a clean way to flatten nested generators and to
capture a sub-generator's final return value — capabilities a plain
re-yielding for loop doesn't fully provide.