Coroutines with send()
Coroutines with send()
Formalized in PEP 342 ("Coroutines via Enhanced Generators"), Python transformed standard generator functions from simple pull-based data producers into push-based coroutines. By using generator.send(value), generator.throw(exception), and generator.close(), a coroutine can act as an event-driven consumer, state machine, or cooperative multitasking actor.
1. The send() Protocol & Execution Model
In a standard generator, yield item produces an item to the consumer. In a coroutine, yield also acts as an expression that receives data pushed from the outside:
The Priming Requirement
None value to a generator that has just been created (GEN_CREATED). Execution must first be advanced to the initial yield expression by calling next(coro) or coro.send(None). Violating this raises:
TypeError: can't send non-None value to a just-started generator.2. Auto-Priming Coroutines with a Decorator
To avoid repetitive manual priming calls, engineers often use a priming decorator:
3. Exception Handling with throw() and Cleanup with close()
External callers can inject exceptions directly into a suspended coroutine using coro.throw() to trigger recovery or reset logic:
4. Building Event-Driven State Machines with Coroutines
Coroutines can implement state machines without monolithic switch-statements or bloated class hierarchies. State transitions occur by passing execution to another coroutine or updating loop targets:
5. From Generator Coroutines to Native Async/Await
Generator-based coroutines served as the historical foundation of asynchronous Python (including early versions of asyncio using @asyncio.coroutine and yield from). In Python 3.5+, PEP 492 formalized native coroutines via async def and await, establishing a clear language-level distinction between iterators that generate data and coroutines that execute concurrent tasks.
6. Architectural Summary Table
| Method | Role | Behavior |
|---|---|---|
next(coro) | Primes or advances | Resumes execution; evaluates yield as None |
coro.send(val) | Passes data in & resumes | Resumes execution; evaluates yield as val |
coro.throw(exc) | Injects exception | Raises exc at current yield point inside frame |
coro.close() | Terminates coroutine | Raises GeneratorExit at current yield point |
Multiple Choice Questions
1.
What error occurs if you call coro.send("data") on a freshly created generator coroutine before advancing it with next() or send(None)? A. ValueError: buffer overflow B. TypeError: can't send non-None value to a just-started generator C. StopIteration D. RuntimeError: deadlock detected
yield expression to receive values. Sending a non-None value immediately raises a TypeError.2.
What is the effect of calling coroutine.throw(ValueError, "Bad Data") on a suspended coroutine? A. It terminates the entire Python process. B. It injects a ValueError into the coroutine at the exact point where it was suspended, allowing it to be caught by a try...except block inside the coroutine. C. It writes the error to a log file without affecting execution. D. It resets the coroutine to the GEN_CREATED state.
throw() raises the specified exception inside the suspended coroutine's frame at the current yield location, giving the coroutine the chance to catch and handle the error.3.
In the expression received = yield current_value, what does current_value represent, and what does received represent? A. Both represent the same integer. B. current_value is sent out to the caller, and received captures whatever value the caller subsequently passes back via send(). C. current_value is private, while received is public. D. received is always None.
yield current_value produces current_value to the caller as the result of next() or send(). When resumed with coro.send(data), the entire yield expression evaluates to data, which is assigned to received.4.
What is the primary function of a "coroutine primer" decorator? A. To compile the coroutine to machine code. B. To automatically call next() or send(None) upon instantiation so the coroutine is immediately ready to accept data via send(). C. To serialize the coroutine over a network socket. D. To convert a synchronous function into an asynchronous one.
next(gen) to advance execution to the first yield, preventing the need for manual priming before sending values.5.
Which PEP introduced native coroutines with async def and await syntax, distinguishing them from generator-based coroutines? A. PEP 8 B. PEP 257 C. PEP 492 D. PEP 3333
async and await syntax in Python 3.5 to create first-class native coroutines distinct from generator objects.Project: Stream Data Processor
Continue learning with hands-on practice, examples, and exercises in the upcoming topic.
Related Lessons
| Previous Lesson | Next Lesson |
|---|---|
| Yield from Expression | Project: Stream Data Processor |
Practice Quiz
Test your understanding of this lesson with 5 questions. Each question has one correct answer.