How does `yield from` forward a caller's send() and throw() to a subgenerator?
answer
- Where is the chain really suspended?
- The delegating frame is only plumbing
- Innermost handler gets first refusal
- Cleanup runs innermost-first on close
- A re-yielding loop swallows sent values
basics
~20 sA 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.
solid answer
~50 sA generator parked on `yield from sub()` is not itself suspended at a `yield` the caller can talk to — the delegation makes it a conduit. A value the consumer sends in travels down the chain and becomes the value of the `yield` expression *inside* the subgenerator; whatever the subgenerator yields next comes straight back out. An exception thrown in is raised at the subgenerator's suspension point, so the subgenerator's own `try`/`except` gets first refusal, and only if it does not handle it does the exception propagate up into the delegating generator at the `yield from` line. Closing the outer generator closes the sub-generator, running its `finally` blocks. Contrast a `for x in sub(): yield x` loop: the outer generator is suspended at its own `yield`, so a sent value lands there and is dropped, the subgenerator resumes with `None`, and a thrown exception hits the outer generator's handlers, never the inner ones.
code
python · 21 linesdef sub():
while True:
received = yield "ready"
if received is None:
continue
print("sub got", received)
def delegating():
yield from sub()
def looping():
for value in sub():
yield value
d = delegating()
print(next(d))
print(d.send("rate=1.5"))
l = looping()
print(next(l))
print(l.send("rate=1.5"))go deeper
You are unlikely to be pushed on two-way generators, but know the headline: while a generator is delegating with yield from, the consumer is really talking to the subgenerator, not to the delegating one.
Explain where the chain's suspension point actually is and trace all three operations to it — a sent value becomes the inner yield expression's value, a thrown exception is raised there, closing tears down inner-first. Contrast each with the re-yielding loop, which drops the sent value.
Demonstrate the operational consequence: pipelines whose stages hold resources must be closed through delegation so finally blocks run, and per-stage recovery means an exception can be handled in a frame the consumer never sees. Be able to say why swapping yield from for a loop is not a safe refactor.
Own the readability tradeoff. Two-way delegation gives composable pipeline stages with local error recovery, at the cost of control flow that no longer reads top to bottom. Decide how deep a chain your codebase tolerates, and when an explicit object with methods is a plainer contract than a generator that can be sent to.
## The claim to test '`yield from` is just a loop' survives exactly as long as nobody pushes anything *into* the generator. The moment a consumer sends a value, throws an exception, or closes the generator, the two constructs behave completely differently, and that difference is the whole point of PEP 380's dedicated syntax. Assume the single-generator side is known: a suspended generator can be resumed with a value, which becomes the result of the `yield` expression it is parked on; it can be resumed with an exception raised at that point; and it can be closed, which raises `GeneratorExit` there so `finally` blocks run. The question here is where those three land when a *chain* of generators is involved. ## Suspension point versus delegation point When a delegating generator executes `yield from sub()`, the value the consumer eventually sees comes from a `yield` inside `sub`. The delegating frame is parked on the `yield from` line, but the interpreter records that it is delegating and routes everything to the sub-iterator. So the *active suspension point of the chain* is inside the subgenerator, several frames down, and that is the point the three operations target: * **A sent value** becomes the value of the `yield` expression inside the subgenerator. If the subgenerator itself delegates further, the value keeps travelling to the innermost suspended generator. * **A thrown exception** is raised at that innermost point. The subgenerator's own `try`/`except` may handle it and carry on yielding — in which case the consumer simply gets the next value and never learns an exception happened. If it does not handle it, the exception propagates outward frame by frame, reaching the delegating generator at its `yield from` line where *its* handlers apply. * **Closing** the delegating generator closes the sub-generator first, so cleanup runs innermost-first, and only then does `GeneratorExit` reach the delegating frame. That innermost-first ordering matters for resource cleanup: a stage that opened a file in a `try`/`finally` gets to close it when the whole pipeline is torn down, even though the consumer only ever held a reference to the outermost generator. ## What the loop does instead With `for row in sub(): yield row`, the outer generator is genuinely suspended at *its own* `yield`. A sent value therefore becomes the result of that outer `yield`, where the loop body ignores it. The next iteration calls the equivalent of plain advancement on `sub`, so the subgenerator's `yield` expression evaluates to `None` — the value the consumer sent has been silently swallowed one frame too early. A thrown exception is raised inside the outer generator at its `yield`, so the outer handlers see it and the subgenerator's handlers never do. The subgenerator will still usually be closed, but only indirectly, when the loop's iterator is collected or the outer generator is closed and the `for` loop unwinds. None of this is visible in a pure pull-based pipeline, which is why the difference goes unnoticed for years: if the only thing anyone ever does is iterate, the loop and the delegation are interchangeable. The bug report arrives when a stage is turned into a two-way stage. ## When the sub-iterator is not a generator Delegation degrades sensibly, but it does degrade. `yield from` accepts any iterable, and a plain iterator has neither a `send` nor a `throw` method. Sending a non-`None` value into a generator suspended on `yield from [1, 2, 3]` raises `AttributeError` complaining that the list iterator has no `send` attribute; sending `None` is fine, because that is ordinary advancement. Throwing an exception in raises it in the delegating generator at the `yield from` line, since there is nothing below that could handle it. Closing works if the sub-iterator happens to have a `close` method and is a no-op otherwise. So 'forwards the whole protocol' is precise only when the operand is itself a generator. ## Reading the consequences Two practical rules follow. First, if a stage is ever going to be fed values or closed by its consumer, splice it in with `yield from`, not a loop — the loop looks equivalent in review and is not. Second, remember that delegation makes control flow non-local: an exception thrown into the outermost generator can be handled several frames down by a stage the consumer has never heard of, and the consumer will see only a value. That is powerful for building pipelines with per-stage error recovery, and confusing when a chain is deep, which is a good argument for keeping delegation chains shallow enough to hold in your head.
- If the subgenerator catches the thrown exception and yields again, what does the caller see?Just the next value. Throwing into a delegating generator resumes the innermost suspended generator by raising there; if that generator's own `try`/`except` handles it and reaches another `yield`, that value is returned to the caller as the result of the throw call. The caller cannot tell from the value alone whether an exception was raised and handled deeper in the chain.
- What happens if you send a non-None value into a generator suspended on `yield from` over a list?`AttributeError`, because a list iterator has no `send` method — delegation forwards the value down, and there is nothing at the bottom that can accept it. Sending `None` works, since that is just ordinary advancement. Full two-way delegation is only meaningful when the operand of `yield from` is itself a generator.
- In what order do cleanup blocks run when a delegating generator is closed?Innermost first. Closing the outer generator closes the sub-generator it is delegating to, which raises `GeneratorExit` at the subgenerator's suspension point and runs its `finally` blocks; only then does the delegating frame unwind and run its own. A chain of stages therefore tears down bottom-up, which is what you want when inner stages hold the file handles.
saying these in an interview costs you the question
- Says yield from is only shorthand for a re-yielding loop
- Thinks a sent value lands in the delegating generator
- Expects the outer handlers to catch a thrown exception first
- Assumes closing the outer generator leaves the inner one open
- Believes any iterable can accept a sent value