2.32. Coroutines

A coroutine is a function that can pause partway through and later resume from where it left off, with all its local variables intact. It mirrors the generator pattern – a function body that can be suspended and resumed – with one change in who drives each resume.

Python’s syntax for writing a coroutine is the async / await keyword pair. async marks the function as a coroutine; await marks the points inside it where pausing is allowed.

2.32.1. Defining a coroutine

A function defined with async def is a coroutine function. Calling it does not run the body; it returns a coroutine object that has not started yet:

async def greet(name):
    print("hello,", name)

coro = greet("Alice")        # body NOT run yet

The coroutine object is paused at the very beginning of the function. Something has to drive it to make the body run – that something is an event loop, a runtime component. MicroPython ships one in the asyncio module. For now, treat the coroutine as “ready to run, waiting for a driver”.

2.32.2. Pausing inside a coroutine

Inside a coroutine, an await expression suspends execution until the awaited value is ready:

async def fetch_and_log():
    data = await read_sensor()
    print("got:", data)

When the body reaches await read_sensor(), the coroutine hands control back to whatever is running it. When read_sensor() finishes, the driver resumes the coroutine on the next line, with the result bound to data.

await is only valid inside a coroutine. Using it in a regular function is a syntax error.

2.32.3. Relationship to generators

Coroutines and generators share the same underlying mechanic. The split is who pulls each resume:

  • A generator yields values; the consumer pulls the next one with next() or by iterating.

  • A coroutine yields control; an event loop schedules the resume when the awaited operation is ready.

If the generator-yield handshake makes sense, the coroutine handshake is the same idea – just driven by an event loop instead of a for loop.

An event loop is a small dispatcher that keeps a list of coroutines waiting on something (a timer, a network event, another coroutine finishing). Each iteration it picks a coroutine whose wait has been satisfied, resumes it until the next await, then records what that coroutine is now waiting for and moves on to another ready one. The result is many tasks making progress concurrently on a single thread – each coroutine voluntarily yields control at its await points, and the loop fills those moments with whatever other coroutines are ready to make progress.

Under the hood, await and yield use the same Python runtime feature for suspending and resuming a function. The keywords differ because the convention around them does: yield hands a value back to a consumer pulling with next(); await hands control to an event loop that schedules the resume when the awaited operation is ready. async / await is essentially newer syntax for the coroutine pattern – older libraries built coroutines on top of the generator machinery directly, using yield from (introduced on Iterators and generators) to delegate suspension between coroutines.

2.32.4. Coroutines need a driver

A coroutine is inert without a runtime to drive it. Defining one is fine; running one needs an event loop. MicroPython’s asyncio module provides that event loop. The Asyncio section covers how to start the loop, schedule coroutines on it, share state between them with locks and events, handle cancellation and timeouts, and shape a real application around the async / await keywords introduced here.