Coroutine Basics

Deep dive · part of Python Generators

Before async/await existed, Python had "classic" coroutines: generators that consume values pushed in with send(). They are still useful for stateful stream processing pipelines that don't need full asyncio.

Before async/await, classic coroutines were generators that received values via send(), enabling incremental parsers, rolling statistics, and protocol state machines without asyncio overhead. They still teach how cooperative multitasking thinks about pause and resume points.

Priming with next(c) or c.send(None) advances to the first yield; subsequent send values become the result of yield expressions inside the coroutine body. This model underpins asyncio tasks even though most new code should prefer async def for I/O.

A coroutine function contains yield and is driven by the caller, not scheduled by an event loop (unless wrapped).

yield expr on the right side of assignment receives values from send().

throw(exc) and close() propagate exceptions for cleanup patterns.

State lives in local variables between sends—natural for accumulators and sliding windows.

StopIteration ends the coroutine; do not confuse with async StopAsyncIteration.

Modern async def replaces manual priming for network and subprocess work.

Classic coroutines are single-threaded and cooperative: no parallelism, just interleaved control flow. They excel when you process a stream item-by-item and need mutable state without a class.

Transition path: if your coroutine awaits network I/O, rewrite as async def and run under asyncio.run. Keep generator pipelines for pure data transformation.

Testing: drive with send sequences and assert yielded snapshots; no event loop required unless integrating with async code.

Forgetting to prime the coroutine before send(), causing TypeError or off-by-one state.

Treating generator coroutines as thread-safe across threads without locks.

Mixing async await syntax into generator coroutines in the same function.

Leaving infinite while True loops without documenting how the caller stops the coroutine.

Encapsulate complex coroutine protocols in classes when more than three yield points interact.

Prefer async/await for network clients; reserve send() pipelines for pure data.

Document priming requirements in the coroutine docstring.

Use finally in the generator for resource cleanup when close() is called.

Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.

The program below demonstrates token counter. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Example: Token counter
# Run in the REPL or save as a .py file and execute with python.
def counter():
    seen = {}
    while True:
        token = yield seen
        seen[token] = seen.get(token, 0) + 1

c = counter(); next(c)
for t in "a b a c a b".split():
    state = c.send(t)
print(state)

This sample walks through sliding window in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# Example: Sliding window
# Run in the REPL or save as a .py file and execute with python.
from collections import deque

def window(size):
    buf = deque(maxlen=size)
    while True:
        x = yield list(buf)
        buf.append(x)

w = window(3); next(w)
for v in [1,2,3,4,5,6]:
    print(w.send(v))

Here is a hands-on illustration of send coroutine. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.

# Classic coroutine: yield receives values via send()
def averager():  # running mean
    total, n = 0, 0  # state
    while True:  # infinite loop
        value = yield (total / n if n else None)  # yield mean, receive next
        total += value  # accumulate
        n += 1  # count

coro = averager()  # create generator
next(coro)  # prime to first yield
print(coro.send(10))  # 10.0
print(coro.send(20))  # 15.0

The program below demonstrates token counter. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Stateful coroutine tallies tokens pushed in
def counter():  # histogram coroutine
    seen = {}  # token -> count
    while True:  # process stream
        token = yield seen  # expose dict, receive token
        seen[token] = seen.get(token, 0) + 1  # increment

c = counter(); next(c)  # start
for t in "a b a c".split():  # tokens
    state = c.send(t)  # push
print(state)  # final counts

« back to Python Generators All tutorials