backend / python core / tricky questions / 27_yield_from_semantics.md

yield from semantics

3 min read source

yield from semantics

The gotcha

yield from gen looks like a shortcut for for x in gen: yield x. It’s not — it delegates the full generator protocol (send, throw, return). The naive for ... yield form silently breaks send() and discards the subgenerator’s return value.

Minimal repro

def sub():
    x = yield 1
    y = yield 2
    return x + y          # subgenerator's return value

# Naive delegation — broken
def parent_naive():
    for v in sub():
        yield v

# Correct delegation
def parent():
    result = yield from sub()
    print("sub returned:", result)
    yield result
g = parent()
print(next(g))            # 1
print(g.send(10))         # 2     — sends 10 into sub's first yield
print(g.send(20))         # sub returned: 30 \n 30

With parent_naive, g.send(10) is just next(g) — the value is discarded because the for loop only consumes via __next__. The naive form is a one-way data flow.

What yield from actually does

def parent():
    yield from sub()

is roughly equivalent to:

def parent():
    _i = iter(sub())
    try:
        _y = next(_i)
    except StopIteration as _e:
        _r = _e.value
    else:
        while True:
            try:
                _s = yield _y           # forward sent value
            except GeneratorExit:
                _i.close(); raise        # forward close
            except BaseException as _e:
                _i.throw(_e)             # forward exception
            else:
                try:
                    _y = _i.send(_s) if _s is not None else next(_i)
                except StopIteration as _e:
                    _r = _e.value         # capture return value
                    break
    # _r is what `yield from` evaluates to

It forwards next, send, throw, close to the subgenerator and captures the return value when the subgenerator ends.

Capturing the return value

def reader():
    line = yield "ready?"
    return f"got: {line}"

def driver():
    result = yield from reader()
    yield result

g = driver()
print(next(g))          # "ready?"
print(g.send("hello"))  # "got: hello"

A generator’s return value doesn’t yield — it sets StopIteration.value. The yield from expression evaluates to that value, giving you a clean way to compose generators that produce a final result.

Composing generators

yield from is what made coroutines possible in pre-async Python:

def chunks(lines):
    chunk = []
    for line in lines:
        chunk.append(line)
        if len(chunk) == 100:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

def process(source):
    yield from chunks(source)   # transparent delegation

Tree traversal becomes elegant:

def walk(node):
    yield node.value
    for child in node.children:
        yield from walk(child)

Difference from async await

yield from was the prototype for await. Both delegate work to a sub-coroutine. In modern Python:

# Generator-based coroutine (legacy, pre-3.5)
@asyncio.coroutine
def fetch_old():
    data = yield from request()   # yield from
    return data

# Native coroutine (3.5+)
async def fetch_new():
    data = await request()         # await replaces yield from for coroutines

For ordinary generators producing values, yield from is still the right tool. For async, use await.

Common pitfalls

  • Forgetting yield from and using for x in sub(): yield x — works for read-only delegation, breaks send/throw.
  • Trying yield from on a non-iterableTypeError. yield from requires an iterable; for a single value, just yield value.
  • yield from of a generator that already started — works, but you only get the rest, not from the beginning.
g = sub()
next(g)                # consumes first yield

def parent():
    yield from g       # picks up where g left off

Interview angle

  • Q: “What does yield from do that a for loop doesn’t?” — full protocol delegation: forwards send, throw, close, captures return value.
  • Q: “Why is yield from needed for coroutines?” — bidirectional data flow; values can be sent into the subgenerator, return values bubble out.
  • Follow-up: “How do you capture a generator’s return value?” — result = yield from gen()return value becomes StopIteration.value, surfaced as the expression value.
  • Follow-up: “What replaced yield from for async?” — await (PEP 492, 3.5+).

See 08_generator_exhaustion.md, 22_generators_iterators.md, 09_async_def_returns_coroutine.md.