# 36. Coroutines and Async

# 36. Coroutines and Async

Coroutines are resumable computations used for asynchronous programming. They let Python code suspend at an `await` point, return control to an event loop, and later resume when the awaited operation has a result.

A coroutine is similar to a generator because both preserve execution state across suspension. The difference is protocol and purpose.

A generator yields values to an iterator consumer.

A coroutine awaits other asynchronous operations and eventually returns one final result.

```python id="o2u8bi"
async def fetch():
    data = await read()
    return data
```

Calling `fetch()` does not run the body to completion. It creates a coroutine object.

```python id="9i5ai1"
coro = fetch()
```

The body runs when the coroutine is awaited or scheduled by an event loop.

## 36.1 Coroutine Function vs Coroutine Object

An `async def` statement creates a coroutine function.

Calling the coroutine function creates a coroutine object.

```python id="rt8ky9"
async def work():
    return 42

coro = work()
```

Conceptually:

```text id="q71j65"
work
    coroutine function object

work()
    coroutine object
        suspended execution state
        code object
        frame or frame-like state
```

This differs from an ordinary function:

```python id="nv1t8j"
def work():
    return 42

work()
```

An ordinary function call runs immediately and returns `42`.

An async function call returns a coroutine object that must later be driven.

## 36.2 `await`

`await` suspends the current coroutine until another awaitable completes.

```python id="wjue72"
async def outer():
    value = await inner()
    return value + 1
```

The expression:

```python id="pbwts5"
await inner()
```

does several things conceptually:

```text id="n2ycfi"
call inner()
obtain awaitable object
suspend current coroutine
let event loop drive awaitable
resume current coroutine with result
```

If the awaited operation raises, the exception is injected back into the awaiting coroutine.

```python id="yc56f3"
async def outer():
    try:
        value = await inner()
    except ValueError:
        value = 0
    return value
```

## 36.3 Awaitables

`await` works on awaitable objects.

Common awaitables include:

```text id="o2vwvk"
coroutine objects
asyncio Task objects
asyncio Future objects
objects implementing __await__
```

A custom object can be awaitable by defining `__await__`.

```python id="f4sd9q"
class Immediate:
    def __await__(self):
        yield
        return 42
```

In practice, most application code awaits coroutines, tasks, and futures created by an async framework.

## 36.4 Coroutine State

A coroutine can be in states similar to generator states:

```text id="qcur3i"
created
running
suspended
closed
```

Use `inspect`:

```python id="84zov8"
import inspect

async def work():
    await other()

coro = work()
print(inspect.getcoroutinestate(coro))
```

Common states include:

```text id="h0yb3u"
CORO_CREATED
CORO_RUNNING
CORO_SUSPENDED
CORO_CLOSED
```

A coroutine cannot be awaited by multiple consumers at the same time. It represents one execution.

## 36.5 Coroutine Frames

A suspended coroutine keeps its execution state.

That state includes:

```text id="o61wj7"
code object
instruction position
local variables
value stack
exception state
currently awaited object
running state
closed state
```

Example:

```python id="1tow3u"
async def process():
    data = await read()
    return transform(data)
```

While suspended at `await read()`, the coroutine must remember:

```text id="985lzm"
current frame
local variables
await target
next instruction after await
```

When `read()` completes, the coroutine resumes and assigns the returned value to `data`.

## 36.6 Coroutines Use the Same Frame Model

Coroutines do not use a separate interpreter. They are executed by the same CPython evaluation loop.

The loop runs until the coroutine:

```text id="p7w1wv"
awaits and suspends
returns
raises
is cancelled
```

Conceptually:

```text id="xzr16h"
coroutine object
    frame state
        bytecode
        stack
        locals

event loop
    resumes coroutine
        evaluation loop runs frame
        stops at await or completion
```

The event loop does scheduling. CPython provides the resumable execution machinery.

## 36.7 Event Loop

An event loop coordinates asynchronous tasks.

In `asyncio`, the event loop manages:

```text id="n7xdfh"
ready tasks
sleep timers
socket readiness
future completion
callbacks
cancellation
exception reporting
```

Example:

```python id="0chlb7"
import asyncio

async def main():
    await asyncio.sleep(1)
    return 42

result = asyncio.run(main())
print(result)
```

`asyncio.run` creates and drives an event loop until `main()` completes.

CPython itself supplies coroutine objects and `await` semantics. `asyncio` supplies one standard event-loop implementation.

## 36.8 Tasks

A coroutine object alone is passive. A task schedules it for execution.

```python id="8et1tq"
import asyncio

async def work():
    await asyncio.sleep(1)
    return 42

async def main():
    task = asyncio.create_task(work())
    result = await task
    return result
```

Conceptually:

```text id="p93t67"
coroutine object
    passive resumable computation

task
    event-loop managed wrapper
    resumes coroutine
    stores result or exception
    supports cancellation
```

A task lets a coroutine run concurrently with other tasks.

## 36.9 Cooperative Concurrency

Async Python uses cooperative concurrency.

A coroutine runs until it reaches an await point.

```python id="8ux1ur"
async def work():
    step1()
    await wait()
    step2()
```

During `step1()`, no other task runs on that event loop unless `step1()` itself awaits or returns control.

At `await wait()`, the coroutine suspends, and the event loop can run another task.

This means CPU-heavy code inside a coroutine can block the event loop:

```python id="g8fwp9"
async def bad():
    while True:
        compute()
```

Without `await`, this coroutine does not yield control.

## 36.10 Async Is Not Parallelism

Async concurrency does not automatically mean CPU parallelism.

One event loop usually runs in one OS thread. It switches between tasks only at suspension points.

Async works well for:

```text id="oell4m"
network I/O
many open connections
timeouts
sleeping
waiting for subprocesses
streaming protocols
cooperative task orchestration
```

It does not make CPU-bound Python bytecode run in parallel.

For CPU-bound work, use:

```text id="r5575k"
native extensions
multiprocessing
process pools
thread pools for blocking calls
free-threaded builds where appropriate
external workers
```

## 36.11 Awaiting a Coroutine

Given:

```python id="h5auqh"
async def inner():
    return 10

async def outer():
    x = await inner()
    return x + 1
```

The flow is:

```text id="fi924o"
outer starts
outer calls inner(), producing inner coroutine
outer awaits inner coroutine
event loop drives inner
inner returns 10
outer resumes with x = 10
outer returns 11
```

The return value crosses the await boundary back into the suspended frame.

## 36.12 Awaiting a Future

A future represents a result that may not exist yet.

```python id="6xf2w8"
future = loop.create_future()
```

A coroutine can await it:

```python id="qze0h0"
value = await future
```

If the future is not complete, the coroutine suspends.

Later:

```python id="nrkg2x"
future.set_result(42)
```

The event loop marks waiting tasks ready. The awaiting coroutine resumes with `42`.

If the future has an exception:

```python id="xzr05h"
future.set_exception(ValueError("bad"))
```

the awaiting coroutine resumes by raising that exception at the await point.

## 36.13 Awaiting a Task

A task is also awaitable.

```python id="w7s6x8"
task = asyncio.create_task(work())
result = await task
```

If the task finishes normally, `await task` returns its result.

If the task raises, `await task` raises the same exception.

If the task is cancelled, `await task` raises `CancelledError`.

## 36.14 Cancellation

Cancellation asks a task to stop.

```python id="j5ce0q"
task.cancel()
```

In `asyncio`, cancellation is delivered by injecting `CancelledError` into the coroutine at an await point.

```python id="lczbrw"
async def work():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        cleanup()
        raise
```

A coroutine may catch cancellation for cleanup, but it should usually reraise it unless it intentionally suppresses cancellation.

Cancellation is cooperative. A coroutine that never awaits may not observe cancellation promptly.

## 36.15 Cleanup With `try/finally`

Use `try/finally` for async cleanup.

```python id="j83mx6"
async def work():
    resource = await acquire()
    try:
        await use(resource)
    finally:
        await release(resource)
```

The `finally` block runs on:

```text id="m2ocv7"
normal completion
exception
cancellation
```

If cleanup itself awaits, the coroutine may suspend during cleanup.

This makes cancellation and cleanup subtle. The coroutine can be in a cancelled state while still executing finalization code.

## 36.16 Async Context Managers

Async context managers use `async with`.

```python id="v2e2wy"
async with lock:
    await work()
```

The object must provide:

```text id="5scc5n"
__aenter__
__aexit__
```

Conceptually:

```python id="dkm7oc"
mgr = lock
value = await mgr.__aenter__()
try:
    await work()
except BaseException as exc:
    suppress = await mgr.__aexit__(type(exc), exc, exc.__traceback__)
    if not suppress:
        raise
else:
    await mgr.__aexit__(None, None, None)
```

Async context managers allow resource acquisition and release to suspend.

## 36.17 Async Iterators

Async iteration uses `async for`.

```python id="hvh27p"
async for item in stream:
    process(item)
```

The object must provide:

```text id="u37g80"
__aiter__
__anext__
```

Conceptually:

```python id="z1gd9g"
ait = stream.__aiter__()

while True:
    try:
        item = await ait.__anext__()
    except StopAsyncIteration:
        break
    process(item)
```

Async iteration is useful for streams where each next item may require I/O.

## 36.18 Async Generators

An async generator is defined with `async def` and `yield`.

```python id="65vpyy"
async def lines(reader):
    async for line in reader:
        yield line.strip()
```

It produces an async iterator.

Use:

```python id="5lhj9g"
async for line in lines(reader):
    print(line)
```

Async generators can both await and yield.

```python id="8c9u5e"
async def gen():
    await asyncio.sleep(1)
    yield 1
```

They combine coroutine suspension with generator-style value production.

## 36.19 `anext`

`anext` retrieves the next item from an async iterator.

```python id="vs0dl0"
item = await anext(ait)
```

This awaits `ait.__anext__()`.

If the async iterator is exhausted, it raises `StopAsyncIteration`.

A default can be supplied:

```python id="8d97ug"
item = await anext(ait, default)
```

## 36.20 `StopAsyncIteration`

Async iterators signal completion with `StopAsyncIteration`.

This is distinct from `StopIteration`.

```text id="prf78x"
normal iterator ends with StopIteration
async iterator ends with StopAsyncIteration
```

`async for` catches `StopAsyncIteration` to exit the loop.

Raising `StopIteration` from async iteration is incorrect.

## 36.21 Coroutine Bytecode

An async function compiles to a code object marked as coroutine code.

```python id="m36blp"
async def f():
    return 1
```

Calling it creates a coroutine object.

An async function with await:

```python id="vb752s"
async def f():
    value = await g()
    return value
```

conceptually includes bytecode for:

```text id="aw12fn"
call g
get awaitable iterator
send into awaitable
suspend if not done
resume with result
store value
return value
```

The exact bytecode changes by Python version. The model remains: `await` is implemented as resumable frame execution.

## 36.22 `SEND` and Await

Modern CPython bytecode uses send-like machinery to drive awaitables.

Conceptually:

```text id="c07p30"
await awaitable:
    get awaitable iterator
    send None or value
    if it yields:
        suspend current coroutine
    if it returns:
        resume with result
    if it raises:
        propagate exception
```

This is related to generator delegation. Coroutines build on the same idea of sending values into suspended computations.

## 36.23 Coroutine Return

A coroutine returns one final value.

```python id="11vkrn"
async def f():
    return 42
```

When awaited:

```python id="q0irxd"
value = await f()
```

the awaiting coroutine receives `42`.

Internally, completion is represented through the awaitable protocol. At user level, `await` turns coroutine completion into an ordinary expression result.

## 36.24 Coroutine Exceptions

If a coroutine raises, awaiting it raises.

```python id="ufon8f"
async def f():
    raise ValueError("bad")

async def main():
    await f()
```

The exception propagates to `main` at the await point.

You can catch it:

```python id="vi414s"
async def main():
    try:
        await f()
    except ValueError:
        return 0
```

The traceback includes coroutine frames involved in the async call chain.

## 36.25 Coroutine Warnings

Creating a coroutine and never awaiting it is usually a bug.

```python id="j9rvzc"
async def f():
    return 1

f()
```

This creates a coroutine object and discards it.

Python may warn:

```text id="e1rugz"
RuntimeWarning: coroutine was never awaited
```

The body never ran.

Correct usage:

```python id="5e8f4p"
await f()
```

or:

```python id="daxsm1"
asyncio.create_task(f())
```

## 36.26 `asyncio.run`

`asyncio.run` is the standard entry point for running an async main function.

```python id="45lxp3"
import asyncio

async def main():
    await asyncio.sleep(1)
    return 42

print(asyncio.run(main()))
```

It creates an event loop, runs the coroutine to completion, handles final async generator cleanup, and closes the loop.

You normally call it once at the top level of a program.

## 36.27 Blocking Calls Inside Async Code

Blocking calls stop the event loop.

Bad:

```python id="upggpj"
import time

async def main():
    time.sleep(5)
```

During `time.sleep`, the event loop cannot run other tasks.

Better:

```python id="j76gyt"
import asyncio

async def main():
    await asyncio.sleep(5)
```

For blocking functions that cannot be made async, use an executor or thread helper:

```python id="bo70ck"
result = await asyncio.to_thread(blocking_function, arg)
```

This keeps the event loop responsive.

## 36.28 Task Groups

Structured concurrency groups related tasks.

```python id="jrw68m"
import asyncio

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(fetch_one())
        tg.create_task(fetch_two())
```

A task group waits for its child tasks. If one task fails, the group coordinates cancellation and raises grouped exceptions as needed.

This is safer than creating tasks without tracking them.

## 36.29 Exception Groups in Async Code

Concurrent tasks can fail together.

Task groups can report multiple failures using exception groups.

```python id="75j6zk"
try:
    async with asyncio.TaskGroup() as tg:
        tg.create_task(a())
        tg.create_task(b())
except* ValueError as group:
    handle_value_errors(group)
```

`except*` can split grouped exceptions by type.

This is important for async systems because independent tasks may fail during the same structured operation.

## 36.30 Async Generators and Cleanup

Async generators need async cleanup.

```python id="6lt8y0"
async def gen():
    try:
        yield 1
    finally:
        await cleanup()
```

If the async generator is not exhausted, it should be closed with `aclose()`.

```python id="hupibv"
agen = gen()
item = await anext(agen)
await agen.aclose()
```

Event loops also include machinery to finalize pending async generators during shutdown.

## 36.31 Async Generator Methods

Async generators support methods similar to generators, but awaitable:

| Method | Meaning |
|---|---|
| `__anext__()` | Get next item asynchronously |
| `asend(value)` | Send value into async generator |
| `athrow(exc)` | Throw exception into async generator |
| `aclose()` | Close async generator |

These methods return awaitables.

Example:

```python id="iuoltd"
value = await agen.__anext__()
await agen.aclose()
```

## 36.32 Coroutines and Reference Lifetime

A suspended coroutine keeps local variables alive.

```python id="3yoa52"
async def work():
    data = bytearray(100_000_000)
    await asyncio.sleep(10)
    return len(data)
```

While sleeping, `data` remains alive because the coroutine may resume and use it.

Retention chain:

```text id="fhhddc"
task
    coroutine
        suspended frame
            local data
```

Clear large locals before long awaits when possible:

```python id="7hxthq"
async def work():
    data = bytearray(100_000_000)
    result = process(data)
    data = None
    await asyncio.sleep(10)
    return result
```

## 36.33 Task Lifetime

A task keeps its coroutine alive.

```python id="4ffgq1"
task = asyncio.create_task(work())
```

Until the task completes or is cancelled and finalized, it references the coroutine and its frame state.

If you create tasks and lose track of them, exceptions may be logged later and resources may remain active longer than expected.

Structured patterns such as task groups help control lifetime.

## 36.34 Async Stack Traces

Async stack traces include suspended coroutine chains.

When an exception crosses `await` boundaries, the traceback shows where it was raised and where it was awaited.

Example:

```python id="uyj5ih"
async def a():
    await b()

async def b():
    await c()

async def c():
    1 / 0
```

The traceback can show the async chain from `a` to `b` to `c`.

This is possible because coroutine frames preserve code object and instruction position information.

## 36.35 `contextvars`

Async code often uses `contextvars` for context-local state.

```python id="ulzcon"
from contextvars import ContextVar

request_id = ContextVar("request_id")

async def handle():
    print(request_id.get())
```

Unlike thread-local storage, context variables are designed to work correctly across async task switches.

Each task can carry its own context.

This is important for:

```text id="ucdnv4"
request IDs
tracing
logging context
database transaction context
auth state
locale settings
```

## 36.36 Async and the GIL

Async code does not remove the GIL.

In traditional CPython:

```text id="vow7l4"
one thread executes Python bytecode at a time per interpreter
async tasks switch cooperatively at await points
```

Async code can handle many I/O-bound tasks efficiently because most time is spent waiting for external events.

It does not make CPU-heavy Python loops execute in parallel.

## 36.37 Async vs Threads

Async and threads solve different problems.

| Feature | Async tasks | Threads |
|---|---|---|
| Scheduling | Cooperative | Preemptive by OS and interpreter checks |
| Switch points | `await` | Thread scheduling and GIL handoff |
| Best for | Many I/O tasks | Blocking APIs, integration with sync code |
| Shared memory | Same thread usually | Shared across threads |
| Race risks | Lower but still possible | Higher |
| CPU parallelism in traditional CPython | No | Limited by GIL for Python bytecode |

Async code is explicit about suspension. Threads can switch at less obvious points.

## 36.38 Async vs Generators

Coroutines and generators share resumable execution, but the protocols differ.

| Feature | Generator | Coroutine |
|---|---|---|
| Definition | `def` with `yield` | `async def` |
| Produces | Multiple yielded values | One final result |
| Consumer | `for`, `next`, `send` | `await`, event loop |
| Completion | `StopIteration` | Await result or exception |
| Main use | Lazy iteration | Async I/O coordination |
| Can await | No | Yes |
| Can yield plain values | Yes | No, except async generators |

A coroutine is not an iterator. A generator is not directly awaitable unless adapted.

## 36.39 A Minimal Coroutine Scheduler

A toy scheduler can show the core idea.

```python id="zytwib"
from collections import deque

class Sleep:
    def __await__(self):
        yield self
        return None

async def task(name):
    print(name, "start")
    await Sleep()
    print(name, "end")

def run(coros):
    ready = deque(c.__await__() for c in coros)

    while ready:
        aw = ready.popleft()
        try:
            next(aw)
        except StopIteration:
            continue
        ready.append(aw)

run([task("a"), task("b")])
```

This is not `asyncio`. It ignores real I/O, timers, futures, cancellation, exceptions, and task state. But it shows the key mechanism:

```text id="6z9pem"
coroutine runs
await yields control
scheduler later resumes it
```

## 36.40 Common Misunderstandings

| Misunderstanding | Correct model |
|---|---|
| Calling an async function runs it | It creates a coroutine object |
| `await` starts a new thread | It suspends current coroutine until awaitable completes |
| Async makes CPU code parallel | It mainly helps cooperative I/O concurrency |
| A coroutine can be awaited repeatedly | A coroutine object represents one execution |
| `asyncio.create_task` is the same as `await` | It schedules concurrently; `await` waits for completion |
| Blocking calls are fine inside async code | They block the event loop |
| Cancellation kills code immediately | It injects an exception cooperatively |
| Async uses a different interpreter | It uses CPython frames and evaluation loop |

## 36.41 Reading Strategy

Start with:

```python id="bltl80"
async def f():
    return 1
```

Then:

```python id="zf4zrn"
async def g():
    value = await f()
    return value + 1
```

Inspect:

```python id="60r9ah"
import dis
import inspect

print(inspect.iscoroutinefunction(f))
coro = f()
print(inspect.getcoroutinestate(coro))
dis.dis(g)
```

Then study:

```text id="gpxkbm"
await
async with
async for
async generators
tasks
cancellation
TaskGroup
contextvars
blocking calls inside async code
```

For each case, track:

```text id="xhs827"
when the coroutine object is created
when the body starts
where it suspends
what object it awaits
how it resumes
what happens on exception or cancellation
when its frame is cleared
```

## 36.42 Chapter Summary

Coroutines are resumable computations used for asynchronous programming. An `async def` call creates a coroutine object. The coroutine body runs only when awaited or scheduled. At each `await`, the coroutine can suspend, preserving its frame state, and later resume with a value or exception.

The core model is:

```text id="r29f1z"
async function call
    ↓
create coroutine object
    ↓
event loop or await resumes coroutine
    ↓
run until await, return, raise, or cancellation
    ↓
await suspends and yields control
    ↓
resume later with result or exception
```

Async execution is built on the same CPython foundations as ordinary execution: code objects, frames, bytecode, exceptions, and object protocols. The event loop supplies scheduling; CPython supplies resumable execution.
