skip to content

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

level: middleimportance: must knowfreq 55%

answer

  1. Two channels out of one generator
  2. Yielded values never reach that name
  3. How does a generator signal it is done?
  4. The ending exception carries a payload
  5. No return statement means None

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.

solid answer

~50 s

The values a subgenerator yields flow out to whoever is consuming the outer generator; `result` never sees them. What `result` binds is the **return value** of the subgenerator — the operand of its `return` statement, or `None` if it just falls off the end. Mechanically, ending a generator raises `StopIteration`, and a `return v` puts `v` on that exception's `value` attribute; `yield from` catches it and evaluates to that value. Written by hand, the delegation would be a `while True` loop around `next()` with `except StopIteration as e: result = e.value`, which is exactly the boilerplate PEP 380 removed. This is what lets a subgenerator act like a function with a stream on the side: it streams rows out and reports a summary back. You cannot fake it by raising `StopIteration` yourself inside a generator body — since Python 3.7 that is converted to `RuntimeError`.

code

python · 16 lines
python
def parse_block(lines):
    accepted = 0
    for line in lines:
        if line.strip():
            accepted += 1
            yield line.strip()
    return accepted

def report(blocks):
    total = 0
    for block in blocks:
        total += yield from parse_block(block)
    print("accepted rows:", total)

blocks = [["7412,ok", "", "7413,ok"], ["7414,ok"]]
print(list(report(blocks)))

go deeper

for a junior

You will mostly meet yield from as loop sugar, so at least remember that the assigned name gets the subgenerator's return value and never its yielded items, and that no return statement means None.

for a middle

Explain the mechanism, not just the rule: a generator ends by raising StopIteration, return v attaches v to that exception, and yield from catches it and evaluates to it. Sketching the hand-written next() plus except StopIteration as exc equivalent is the answer interviewers are hoping for.

for a senior

Show judgement about where the result is readable — inside a delegating generator, never from a for loop or list() — and design stages accordingly. Know that PEP 479 turned an escaping StopIteration into RuntimeError in 3.7 and why silent truncation was the worse bug.

for a principal

Weigh the pattern against alternatives your team can read: a per-stage summary returned through yield from is elegant but invisible to ordinary consumers, so it works for internal pipelines and is a poor public API. Decide when a plain object with a stream method is the clearer contract.

## Two channels, not one A generator has two distinct output channels, and `result = yield from subgen()` is the place where the difference becomes unavoidable. The **stream** channel is everything the subgenerator yields; those values do not stop at the delegating generator, they pass straight through to whatever is iterating it. The **result** channel is a single value produced once, when the subgenerator terminates. That is what the `yield from` expression evaluates to, and what `result` binds. So in a payroll CSV import where an inner generator cleans and emits the usable rows of one block, the rows go to the consumer while the block's accepted-row count comes back to the delegating generator, which can accumulate a total across blocks. One pass, no shared mutable accumulator, no second return trip through a list. ## The mechanism underneath Every generator ends by raising `StopIteration`. A bare `return`, or falling off the end of the body, raises `StopIteration` with no argument. A `return v` raises it with `v` attached, readable as `StopIteration.value` — and `value` is `None` for the no-argument case, which is why a subgenerator with no `return` binds `None` rather than raising or leaving the name unbound. `yield from` is defined to catch that `StopIteration`, pull `value` off it, and become that value. The equivalent hand-written delegation, which is what people wrote before Python 3.3, is roughly: ```python it = iter(subgen()) while True: try: yield next(it) except StopIteration as exc: result = exc.value break ``` That sketch already shows two of the reasons the syntax exists: the boilerplate is easy to get wrong, and it is *still* lossy — it moves values and the result, but not values sent in or exceptions thrown in. ## Why you cannot forge the result Since a return value travels as an exception attribute, an obvious-looking trick is to `raise StopIteration(v)` inside the generator instead of returning. That has not worked since PEP 479 became mandatory in Python 3.7: a `StopIteration` that escapes a generator body is replaced by `RuntimeError: generator raised StopIteration`. The motivation was exactly this ambiguity — a `StopIteration` leaking out of a nested call used to silently truncate the generator, and truncation-by-accident is a far worse bug than a loud `RuntimeError`. The rule for you is simple: end a generator with `return`, never by raising `StopIteration`. ## Where the result is visible and where it is not The asymmetry catches people out. Inside a delegating generator, `yield from` gives you the result directly. Outside, a plain `for` loop over a generator throws the return value away, because `for` treats `StopIteration` purely as the stop signal. `list(gen())` likewise contains only the yielded values. If a caller that is *not* itself a generator needs the result, it has to drive the generator with `next()` and catch `StopIteration` to read `value` — which is precisely why this idiom pairs a delegating generator with the subgenerator, rather than exposing the result to arbitrary callers. The same asymmetry explains a frequent misreading of the expression. `result = yield from subgen()` looks like an assignment of *the iteration* to a name, so people expect a list, or the last yielded value. It is neither. If you want the yielded values in the delegating generator as well as passing them on, you cannot use `yield from` — you need an explicit loop that yields each item and keeps a copy, and then you must read the result yourself. ## Practical shapes The pattern shows up wherever a stage both produces a stream and has something to report: a parser stage returning how many records it accepted, a reader returning the byte offset it stopped at, a recursive walker returning the size of the subtree it just streamed. In each case the return value is a *summary of the stage*, computed where the information already is, instead of being smuggled out through an argument that the stage mutates. Interviewers like the question because the wrong answers — 'a list', 'the last value', 'the generator object' — all reveal that the candidate has only ever used `yield from` as loop sugar. ## Results chain upward too Because the result is just the value of an expression, it composes. A delegating generator can add the results of several subgenerators together and `return` the total, and whatever delegates to *it* binds that total the same way. A recursive walker written this way streams leaves outward while each level returns the count of the subtree it just produced, so the top-level call ends with the whole count without a single shared counter or an out-parameter. That is the shape to reach for when a stage needs to report on what it streamed.

  • What does `result` bind when the delegated generator has no `return` statement?
    `None`. Falling off the end of a generator body raises `StopIteration` with no argument, and that exception's `value` attribute is `None`, so the `yield from` expression evaluates to `None`. It is not an error and there is no warning — which is why a subgenerator whose result you actually consume should return explicitly on every exit path, including early ones.
  • Can a plain `for` loop over a generator see that return value?
    No. `for` and `list()` treat `StopIteration` as nothing but the stop signal and discard the exception, so the return value is lost. Only a delegating generator using `yield from`, or code that drives the generator with `next()` and catches `StopIteration` to read its `value`, can see it.
  • Why does raising `StopIteration` inside a generator to set the result no longer work?
    PEP 479 made it an error, mandatory since Python 3.7: a `StopIteration` escaping a generator body is replaced by `RuntimeError: generator raised StopIteration`. Before that, a stray `StopIteration` from any nested call would silently end the generator, truncating the stream with no sign of failure. Use `return value`, which is the supported way to set it.

saying these in an interview costs you the question

  • Says result binds a list of the yielded values
  • Says result binds the last value yielded
  • Thinks a for loop over the generator can read the return value
  • Raises StopIteration(value) instead of using return
  • Assumes an absent return statement is an error

context