Python yield from: Delegating to Subgenerators
How python yield from delegates iteration to subgenerators, forwards send() and throw() calls, and enables coroutine composition in Python.
python yield from requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
yield from in Python delegates iteration from one generator to another. When a generator contains yield from subgen, the outer generator suspends and forwards all iteration to subgen, and the caller receives values directly from the subgenerator. This is more than syntactic sugar for a for loop: it also forwards send(), throw(), and close() calls, which makes it essential for building coroutine pipelines.
What yield from Does at the Call Site
When you write:
def outer(): yield 1 yield from inner() yield 2 def inner(): yield "a" yield "b"
Calling list(outer()) produces [1, 'a', 'b', 2]. The yield from statement suspends outer() and lets inner() drive the iteration. The caller never sees a nested generator object; values from inner() appear as if outer() produced them directly.
The delegation is transparent to the caller. When inner() is exhausted, yield from catches the resulting StopIteration and resumes outer() after the delegation line. Other exceptions raised inside inner() propagate to the caller.
How yield from Differs from a for Loop
A common alternative is:
def outer(): yield 1 for value in inner(): yield value yield 2
For simple iteration, both approaches produce the same sequence. The difference appears when the caller uses send() to pass values back into the generator. With a for loop, the send() value goes to the outer generator only; the inner generator never receives it. With yield from, the value is forwarded directly to the innermost suspended generator.
This forwarding behavior is what makes yield from the correct tool for building generator pipelines, not just a shorter way to write a loop.
Bidirectional Communication with send() and throw()
yield from forwards three operations:
send(value)— the value is passed to the subgenerator's current yield pointthrow(exc)— the exception is raised inside the subgeneratorclose()— the subgenerator is closed
Consider a generator that expects input:
def accumulator(): total = 0 while True: value = yield total total += value def proxy(): yield from accumulator()
Calling proxy().send(5) sends 5 into accumulator(), not into proxy(). The proxy has no yield point of its own while the subgenerator is active.
This is the behavior that makes yield from the foundation for coroutine-style code. Without forwarding, building a chain of generators that pass data in both directions would require manual plumbing in every layer.
Using yield from in Coroutines
Before async/await became standard, yield from was the mechanism for composing coroutines. A coroutine could delegate to another coroutine with:
# Conceptual example: composing generator-based coroutines def fetch_user(user_id): # api_request stands in for any I/O operation that yields to the event loop response = yield from api_request(f"/users/{user_id}") return response def fetch_user_with_posts(user_id): user = yield from fetch_user(user_id) posts = yield from fetch_posts(user["id"]) return {"user": user, "posts": posts}
The functions api_request and fetch_posts stand in for whatever I/O mechanism the application uses. The important part is that each yield from suspends the current coroutine until the delegated operation completes, and the return value of the subgenerator becomes the value of the yield from expression.
In modern Python, async def and await replace this pattern for new code. But yield from remains relevant for generator-based libraries and for understanding how await behaves internally.
Runtime Cost and When It Matters
yield from adds a small amount of overhead compared to a direct yield in a single generator, because each delegation involves an extra frame on the call stack. In practice, the cost is negligible for typical I/O-bound or pipeline workloads. The overhead only becomes measurable in tight loops that yield millions of values, where a plain for loop with yield may be marginally faster.
The more important consideration is memory. yield from does not materialize the subgenerator's output into a list; values flow one at a time. This keeps memory usage flat regardless of how many values the subgenerator produces.
Common Mistakes and Edge Cases
One frequent mistake is using yield from with a non-iterable value. yield from requires an iterable; passing an integer raises TypeError.
Another edge case: if the subgenerator returns a value, that value is the result of the yield from expression:
def inner(): yield 1 return 42 def outer(): result = yield from inner() print(result) # 42
The return value is not yielded to the caller; it is assigned to the expression. Confusing this with yield is a common source of bugs.
Also, a function containing yield from is automatically a generator function. Calling it returns a generator object; the body does not execute until iteration begins. Trying to use yield from in a function that you expect to run eagerly will silently produce a generator instead.
When Not to Use yield from
If you only need to flatten one level of iteration and never send values back, a for loop is clearer to most readers. yield from shines when:
- you are building a pipeline of generators that pass data in both directions
- you are composing coroutines that delegate to each other
- you want to expose a subgenerator's output without copying it into a list
If the delegation is one-way and the subgenerator is short, the for loop is often more readable. The choice is about intent, not capability.