Skip to main content

Classic Coroutines

Source: Fluent Python, 2nd Edition — Chapter 19 by Luciano Ramalho (through termination and throw / close)

Davis and van Rossum: native async def (3.5) sits on the classic generator coroutine of 2.5/3.3. This lesson is that foundation. Chapter 22 is asyncio.

To yield means both produce and give way. A generator’s caller pulls with next. A classic coroutine is a generator the caller pushes with .send(datum). The pending yield expression becomes that datum. Often nothing useful is produced (x = yield yields None). Cooperative multitasking is just this: yield to a scheduler.

Three kinds:

KindHow you write itDelegate with
Classicyield in an expression, .sendyield from
Generator-based (3.5)@types.coroutineawait
Nativeasync defawait

Native/generator-based are for async I/O. Classic still does things they cannot, and vice versa. After PEP 380 (3.3) classic coroutines froze; PEP 492 is a different story.

PEP 342 (2.5): yield in expressions, .send, .throw, .close. PEP 380: return a value from a generator; yield from. Those last two wait for part 19b.

Basic behavior​

def simple_coroutine():
print('started')
x = yield
print('received', x)

Call the function → generator object. Prime with next(coro) or coro.send(None) so it runs to the first yield. Then send(42) resumes; x is 42. Running off the end raises StopIteration.

send of a non-None into GEN_CREATED is TypeError: can't send non-None value to a just-started generator.

States (inspect.getgeneratorstate):

  • GEN_CREATED — not started
  • GEN_RUNNING — only visible from another thread
  • GEN_SUSPENDED — paused at yield
  • GEN_CLOSED — finished
b = yield a

The right-hand side runs (yields a) and stops. b is assigned only when the next send (or next) resumes that line. Each phase ends at a yield and the next starts on the same line.

wrap should next(g) then send. If recv stores x on a list the caller can read, or send’s return is unused after the last yield — capture x before the function ends (e.g. log.append(x) after the assignment).

Running average​

total and count stay local — no closure object, no self. Infinite while True: term = yield average. Terminate later with .close() or a sentinel.

Priming decorator​

Easy to forget next. A @coroutine wrapper calls func(*args), next(gen), returns gen. After that, getgeneratorstate is already GEN_SUSPENDED.

yield from (later) also primes; do not stack that with this decorator. asyncio.coroutine (3.4) did not prime for that reason. Tornado’s gen decorator is a different hook.

The “wrap does not next” check is a regex over the whole file — keep next only inside coroutine, not in wrap or averager.

Termination and exceptions​

Unhandled error in the body → caller of send/next sees it; the coroutine is dead (GEN_CLOSED); another send is StopIteration.

Sentinel: None, Ellipsis, or even the StopIteration class as a value.

.throw(exc) raises at the paused yield. If handled, execution continues to the next yield and that value is the result of .throw. If not, it propagates and the generator closes.

.close() raises GeneratorExit at yield. Swallowing it or running to StopIteration is quiet. Do not yield after GeneratorExit (RuntimeError). Other exceptions propagate.

Cleanup: try/finally around the loop. Nested throw into subgenerators is why yield from exists (19b).

Quiz

Coroutine states

1 / 2

Right after g = coro(); before next(g), send(1) raises

Pick an answer to see if you are right.

demo must keep looping after Skip. Pass log in via a default or closure so the body can append.