38.6 From Generator Coroutines to async/await

The evolution from generator-based coroutines to the modern async/await syntax represents a significant paradigm shift in Python’s approach to asynchronous programming. While generators provided a clever and powerful foundation, they were ultimately a repurposing of a feature designed for iteration, not a purpose-built solution for concurrency. The async/await syntax, introduced in Python 3.5, offered a cleaner, more explicit, and less error-prone model. The Generator-Based Coroutine Pattern Before async/await, coroutines were implemented using generators. A function containing a yield expression could be paused, and its execution could be controlled via .send(), .throw(), and .close() methods. This made it possible to build event loops that managed many such generators concurrently.

38.5 yield from and the Event Loop Analogy

While yield from might seem like a simple delegation mechanism, its true power is unlocked when viewed through the lens of an event loop. It provides a primitive, coroutine-native way to manage asynchronous control flow, acting as a microcosm of the more complex event loops found in frameworks like asyncio. Understanding this analogy is crucial for grasping why native coroutines (async/await) were designed the way they were and how they build upon these concepts.

38.4 close(): Triggering GeneratorExit

The close() method provides a mechanism to gracefully terminate a generator coroutine from the outside. When invoked, it raises a GeneratorExit exception inside the generator at the point where its execution is suspended—typically at a yield expression. This exception is not meant to be caught and handled like a typical error; rather, it signals to the generator that it should perform any necessary cleanup (like closing open files or network connections) and then terminate. If the generator handles the exception and yields another value instead of stopping, the close() method will raise a RuntimeError, as this violates the protocol for graceful termination.

38.3 throw(): Injecting Exceptions into a Generator

The throw() method provides a powerful mechanism for injecting exceptions into a generator’s execution frame, effectively allowing external code to force the generator to handle an error condition at its current suspension point. This transforms the traditional one-way communication of generators (yielding values out) into a two-way street, where the caller can not only receive data but also send signals—in this case, error signals—back into the generator’s context. How throw() Works Mechanically When generator.throw(exc_type, exc_value, exc_traceback) is called, the provided exception is raised at the precise point where the generator was paused, typically at a yield expression. The generator’s execution resumes not to produce the next value, but to immediately handle the injected exception. This allows the generator to perform cleanup, log the error, yield a final value, or even suppress the exception entirely.

38.2 send(): Sending Values into a Paused Generator

The send() method is the fundamental mechanism for two-way communication with a generator. While next() is used to retrieve a value from a generator, send() allows you to push a value into a generator while it is paused at a yield expression. This transforms a generator from a simple producer of values into a co-operative routine that can both consume and produce data, hence the term “coroutine.” The Mechanics of send() When a generator function is first called, it returns a generator object in a state prior to the first yield. You cannot send a value into this initial state because there is no yield expression ready to receive it. Therefore, the first interaction with a generator must either be a call to next() or send(None). This “primes” the generator, advancing its execution to the first yield point.

38.1 Generator-Based Coroutines with yield

Generator-based coroutines, built upon the foundational yield expression, represent the original incarnation of coroutines in Python. Unlike native async/await coroutines, which are designed for explicit asynchronous I/O, generator-based coroutines are a more general-purpose concurrency tool. They operate by suspending their execution state, allowing other code to run, and then resuming precisely where they left off. This suspension and resumption are orchestrated through a trio of special methods on the generator object: send(), throw(), and close().

— joke —

...