Skip to main content

yield from and Simulations

Source: Fluent Python, 2nd Edition — Chapter 19 by Luciano Ramalho (return, yield from, discrete events)

Part 19a stopped at .throw / .close. This half: returning a value (PEP 380), the yield from channel, a tiny discrete-event taxi, and Generator[Yield, Send, Return] variance. await reuses this machinery with extra checks (PEP 492); study this if you care how asyncio ticks.

Returning a value​

A coroutine that only accumulates may yield None each time and return a namedtuple when a sentinel (None) breaks the loop. Before 3.3, return x in a generator was a SyntaxError.

The value rides on StopIteration.value. Ugly by design: exhausted generators still raise StopIteration; for already swallows that. yield from catches it and the .value is the yield from expression. You cannot type yield from at the REPL (not inside a function).

exc.value is the returned tuple. send(None) must actually terminate the loop.

yield from​

Not “yield, from a list.” The subgenerator talks directly to the caller; the delegating generator is frozen until the subgenerator returns. await is stricter (native coroutine, awaitable target). yield from works in any generator; the target is any iterable (yield from [1, 2, 3]).

PEP names: caller (client), delegating generator, subgenerator. iter(x) first — even a plain iterator with only __next__ works; .send / .throw / .close are forwarded when they exist.

Happy path (no throw/close), RESULT = yield from EXPR is: prime sub with next; loop yield what it yields; send what the client sent; on StopIteration the .value is RESULT. Auto-prime means do not wrap the subgenerator in the 19a @coroutine decorator.

Must terminate the subgenerator. In the grouper example, group.send(None) ends averager so results[key] = yield from ... actually assigns. Skip that None and the dict stays empty — the next outer loop throws away the stuck pair.

Exceptions: non-GeneratorExit → sub .throw; close / GeneratorExit → sub .close if present. Nested try in the PEP expansion is for language lawyers; pipelines of yield from still need an outermost next/send (or a for).

Without send(None), results[key] is never set and wrap crashes — that is the lesson.

Discrete event simulation​

The clock jumps to the next event (taxi pick-up / drop-off), not 60 fps. Turn-based games are DES; real-time shooters are continuous. Coroutines are a good fit: each process (model entity, not OS process) is a generator. SimPy does this for real; asyncio’s loop is the same idea.

A taxi: time = yield Event(...) — the sim sends the new clock when that event is due. PriorityQueue orders Event.time. run: prime each taxi, put first events, then get earliest, send(next_time), reschedule or delete on StopIteration. while/else and try/else (Chapter 18) keep the loop readable.

Drive one taxi from the console with send(_.time + delay) to feel the handshake.

wrap already drives; you implement taxi so each send gets the next action. time = yield Event(...) so the next clock is what the sim sent.

Generic Generator[Y, S, R]​

Pull-only generators: Iterator[T_co]. Classic coroutines: Generator[YieldType, SendType, ReturnType]. Yield and return are covariant (outputs). Send is contravariant (input): a client that sends float cannot use a coroutine that only accepts int. Skip if variance is still mush; the thumbs from Chapter 15 apply.

Guido’s three styles: pull (iterators), push (averager), tasks (taxis / Beazley). Native coroutines take over async; classic remains for these niches and for reading await.