skip to content

yield from and Delegation

yield from hands the whole protocol to a subgenerator: values out, send() and throw() in, and its return value as the expression result. It is how you split or flatten nested generators.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does `yield from` do inside a Python generator function?

level: juniorimportance: must knowfreq 70%

answer

  1. Splitting one long generator into stages
  2. Removes an inner re-yielding loop
  3. Calls iter() on whatever follows it
  4. Delegates the protocol, not only values

basics

~20 s

yield from <iterable> yields every value that iterable produces, replacing an explicit for-loop that re-yields inside a generator function. Unlike that loop, it also forwards send, throw and close to a subgenerator and binds its return value.

solid answer

~50 s

`yield from expr` calls `iter(expr)` and hands control of the generator over to that iterator: every value the sub-iterator produces flows straight out to whoever is consuming the outer generator, until it is exhausted. For plain iteration it is equivalent to `for x in expr: yield x`, and that is how most people meet it — the one-line way to splice a nested generator into an outer one when a long generator is split into stages. But it is not only sugar. When the sub-iterator is itself a generator, `yield from` delegates the rest of the protocol: values a caller sends in reach the subgenerator's own `yield` expression, exceptions thrown in are raised inside the subgenerator, closing the outer one closes the inner one, and the subgenerator's `return` value becomes the value of the whole `yield from` expression.

code

python · 16 lines
python
def batches(rows, size):
    for i in range(0, len(rows), size):
        yield rows[i:i + size]

def loop_version(groups):
    for group in groups:
        for row in group:
            yield row

def delegate_version(groups):
    for group in groups:
        yield from group

rows = [("emp", n) for n in range(5)]
print(list(loop_version(batches(rows, 2))))
print(list(delegate_version(batches(rows, 2))))

go deeper

for a junior

Be ready to say in one sentence that yield from x yields everything x produces, and to write the equivalent for loop on the whiteboard. Knowing it works on any iterable, not only generators, is the follow-up you should not miss.

for a middle

Explain the mechanics: iter() is called on the operand, the delegating frame is suspended and bypassed while the sub-iterator runs, and the delegation carries the return value plus sent values, thrown exceptions and close. Name the loop as the lossy alternative.

for a senior

Show where you actually reach for it — cutting an overgrown generator into stages, recursive traversal — and where you would not: deep chains resume every level per item, so per-item cost grows with depth. Be able to say what happens at consumption time versus call time.

for a principal

Own the design tradeoff: delegation makes a generator pipeline composable but also makes control flow non-local, since a value sent by the consumer can travel several frames down. Have a view on how deep a delegation chain your team should tolerate before an explicit loop or a flattened stage reads better.

## The two objects involved A **generator function** is any `def` whose body contains `yield` or `yield from`. Calling it runs none of the body; it builds and returns a **generator object**, a resumable frame that produces values one at a time when something calls `next()` on it. `yield from` is a statement you write *inside* a generator function, and it always concerns two generator objects (or a generator and a plain iterator): the **delegating** generator that contains the `yield from`, and the **sub-iterator** it delegates to. ## What the syntax does `yield from expr` first evaluates `expr` and calls `iter()` on the result, so the operand may be any iterable at all — a list, a `range`, a file object, another generator. It then suspends the delegating generator and pumps the sub-iterator: each value the sub-iterator produces is handed directly to whoever is consuming the delegating generator, with no code in the delegating frame running in between. When the sub-iterator is exhausted, execution continues on the line after the `yield from`. If `expr` is not iterable you get `TypeError: 'int' object is not iterable` at the moment the delegating generator reaches that line — which, because generators are lazy, is not when you called the generator function. Written outside any function it is a `SyntaxError: 'yield from' outside function`, and inside an `async def` it is `SyntaxError: 'yield from' inside async function`: asynchronous delegation is a different mechanism, not this one. ## Why it exists beyond the loop The obvious use is refactoring. A generator that has grown to fifty lines can be cut into stages, and the outer one splices a stage back in with a single line instead of an inner `for` loop that re-yields. Recursive traversal is the same trick applied to itself: a generator that walks a nested structure delegates to itself for each child. The reason PEP 380 added dedicated syntax rather than leaving people with the loop is that the loop is a *lossy* pipe. It moves values outward and nothing else. Four things travel through `yield from` that the loop drops or misroutes: 1. **The return value.** A `return v` inside the sub-generator makes `v` the value of the `yield from` expression, so `total = yield from stage()` is meaningful. A re-yielding loop discards it. 2. **Sent values.** When a caller pushes a value into the delegating generator, it lands at the `yield` expression inside the *subgenerator*. Through the loop the subgenerator would only ever see `None`. 3. **Thrown exceptions.** An exception thrown into the delegating generator is raised at the subgenerator's suspension point, giving the subgenerator's own `try`/`except` a chance to handle it. 4. **Closing.** Closing the delegating generator closes the sub-generator too, so its `finally` blocks and cleanup run. That is why the usual one-sentence summary is that `yield from` delegates the *whole generator protocol*, not just the values. ## What it is not It is not `return`. `return sub()` from a generator function does not produce the subgenerator's values — it ends the outer generator and stuffs the generator object into the resulting `StopIteration`. It is not `yield sub()` either, which produces exactly one item: the subgenerator object itself. And it is not `await`: `await` belongs to coroutine functions and resolves an awaitable to a single result, while `yield from` produces a stream of values from an iterator. ## Cost `yield from` is somewhat faster than the equivalent Python-level loop, because resuming a level of delegation is handled inside the interpreter instead of executing loop bytecode in the delegating frame. It is not free, though: it does **not** collapse the chain. Every level of a nested `yield from` chain is still resumed for every single item, so per-item cost grows roughly linearly with the depth of the chain. A two- or three-stage pipeline is unmeasurable; a fifty-level recursive descent over millions of items is not. ## The mental model While the delegating generator is parked on a `yield from`, it is not really running — it is a piece of plumbing. Everything the consumer does reaches the subgenerator, and everything the subgenerator produces reaches the consumer. The delegating frame wakes up again only when the subgenerator finishes, and what it wakes up with is that subgenerator's return value.

  • Can `yield from` delegate to a list, or only to another generator?
    To anything iterable — it calls `iter()` on the operand, so lists, tuples, ranges, files and generators all work. The difference is what the extra delegation gets you: a plain iterator has no `send` or `throw` method, so sending a non-`None` value into a generator suspended on `yield from [1, 2]` raises `AttributeError` about the list iterator, and a thrown exception is raised in the delegating generator itself. Full protocol delegation only means something when the sub-iterator is a generator.
  • Does writing `yield from` make the enclosing function a generator function?
    Yes. Any `yield` or `yield from` anywhere in a `def` body compiles that function into a generator function. Calling it executes none of the body and returns a generator object; the body starts running only on the first `next()`. That is why a `yield from` over a non-iterable raises `TypeError` at consumption time rather than at call time.
  • How is `yield from sub()` different from `return sub()` inside a generator function?
    `return sub()` ends the delegating generator immediately and produces no values from `sub()` at all — the generator object it built is attached to the `StopIteration` that terminates the outer generator, where nobody normally looks. `yield from sub()` runs `sub()` to exhaustion and passes each of its values out to the consumer. `yield sub()` is a third thing again: it produces exactly one item, the subgenerator object itself.

A receptionist who does not take a message and repeat it back, but transfers the caller straight through to the right desk: for the length of the call, everything said in either direction goes end to end.

saying these in an interview costs you the question

  • Says yield from returns the subgenerator's values as a list
  • Thinks yield from sub() is the same as return sub()
  • Believes the operand must be a generator, not any iterable
  • Claims it is pure shorthand with no behavioural difference
  • Confuses yield from with await on a coroutine object
  • Expects the body to run before the first next() call

context

open as a page

What value does `result = yield from subgen()` bind in a Python generator?

level: middleimportance: must knowfreq 55%

basics

~20 s

It binds the subgenerator's return value, not its yielded values. Those went straight to the consumer. When the subgenerator finishes, its return value rides out on the StopIteration that ends it, and yield from unpacks it. No return statement means None.

open as a page

How does `yield from` forward a caller's send() and throw() to a subgenerator?

level: middleimportance: should knowfreq 45%

basics

~20 s

A generator suspended on yield from is a transparent pipe: a sent value resumes the subgenerator at its own yield expression, a thrown exception is raised there, and closing the outer closes the inner. A re-yielding loop forwards none of it.

open as a page

What breaks in a recursive `yield from` flattener on strings or deeply nested data?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Strings are the trap: iterating a one-character string yields that same string, so a flattener recursing into anything iterable never terminates and raises RecursionError. Deep nesting also hits the recursion limit, and per-item cost grows with delegation depth.

open as a page