How CPython Implements coroutine in C

PyCoroObject, the shared generator machinery, CO_COROUTINE, the RETURN_GENERATOR path, the awaitable protocol, cr_await, and unawaited-coroutine handling traced through the CPython 3.15 source.

Python’s coroutine type (types.CoroutineType, the object an async def function returns when called) is not a separate implementation: there is no coroutineobject.c. CPython reuses the generator machinery in Objects/genobject.c and distinguishes a coroutine by the CO_COROUTINE flag on its code object. A coroutine is a PyCoroObject, which is just a PyGenObject with the cr_ prefix and one extra field for origin tracking.

The object struct

Generators, coroutines and async generators all share a single head macro, _PyGenObject_HEAD(prefix), defined in Include/internal/pycore_interpframe_structs.h. A coroutine substitutes the cr prefix:

c
// Include/internal/pycore_interpframe_structs.h
#define _PyGenObject_HEAD(prefix)                                           \
    PyObject_HEAD                                                           \
    PyObject *prefix##_weakreflist;                                         \
    PyObject *prefix##_name;                                                \
    PyObject *prefix##_qualname;                                            \
    _PyErr_StackItem prefix##_exc_state;                                    \
    PyObject *prefix##_origin_or_finalizer;                                 \
    char prefix##_hooks_inited;                                             \
    char prefix##_closed;                                                   \
    char prefix##_running_async;                                            \
    int8_t prefix##_frame_state;                                            \
    _PyInterpreterFrame prefix##_iframe;

struct _PyCoroObject {
    _PyGenObject_HEAD(cr)
};

So a PyCoroObject carries the same state as a generator: weakref list, name/qualname, saved exception state, frame state, and the embedded interpreter frame (cr_iframe) that holds locals and the evaluation stack. The one coroutine-specific member is cr_origin_or_finalizer, used for coroutine origin tracking (PEP 492 / sys.set_coroutine_origin_tracking_depth). The public handle is an opaque pointer:

c
// Include/cpython/genobject.h
typedef struct _PyCoroObject PyCoroObject;
PyAPI_DATA(PyTypeObject) PyCoro_Type;
#define PyCoro_CheckExact(op) Py_IS_TYPE((op), &PyCoro_Type)

CO_COROUTINE

The distinction between a generator and a coroutine lives in the code object. async def produces CO_COROUTINE (0x0080); plain def with yield produces CO_GENERATOR; async def with yield produces CO_ASYNC_GENERATOR. There is also CO_ITERABLE_COROUTINE (0x0100), set by types.coroutine, which keeps a generator-shaped object but makes it awaitable:

c
// Include/cpython/code.h
#define CO_GENERATOR            0x0020
#define CO_COROUTINE            0x0080
#define CO_ITERABLE_COROUTINE   0x0100
#define CO_ASYNC_GENERATOR      0x0200

_Py_MakeCoro reads those flags and picks the type. A CO_ITERABLE_COROUTINE object stays a PyGen_Type instance but is treated as a coroutine by the awaitable helpers:

c
// Objects/genobject.c (_Py_MakeCoro)
PyObject *_Py_MakeCoro(PyFunctionObject *func)
{
    int coro_flags = ((PyCodeObject *)func->func_code)->co_flags &
        (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR);
    assert(coro_flags);
    if (coro_flags == CO_GENERATOR) {
        return make_gen(&PyGen_Type, func);
    }
    if (coro_flags == CO_ASYNC_GENERATOR) {
        ...
    }
    assert (coro_flags == CO_COROUTINE);
    PyObject *coro = make_gen(&PyCoro_Type, func);
    ...
    /* origin tracking */
    if (origin_depth == 0) {
        ((PyCoroObject *)coro)->cr_origin_or_finalizer = NULL;
    } else {
        ...
        ((PyCoroObject *)coro)->cr_origin_or_finalizer = cr_origin;
    }
    return coro;
}

make_gen allocates with PyObject_GC_NewVar, sized to the code object’s local/stack slots, and leaves the frame in FRAME_CLEARED state until the caller fills it in.

Creation: RETURN_GENERATOR

Coroutines are not constructed eagerly. Calling an async def function runs until the first suspension point; the RETURN_GENERATOR bytecode (shared by all three generator kinds) converts the current frame into a generator/coroutine object and hands control back to the caller:

c
// Python/bytecodes.c (RETURN_GENERATOR)
inst(RETURN_GENERATOR, (-- res)) {
    assert(PyStackRef_FunctionCheck(frame->f_funcobj));
    PyFunctionObject *func = (PyFunctionObject *)PyStackRef_AsPyObjectBorrow(frame->f_funcobj);
    PyGenObject *gen = (PyGenObject *)_Py_MakeCoro(func);
    ERROR_IF(gen == NULL);
    ...
    _PyInterpreterFrame *gen_frame = &gen->gi_iframe;
    frame->instr_ptr++;
    _PyFrame_Copy(frame, gen_frame);
    assert(frame->frame_obj == NULL);
    gen->gi_frame_state = FRAME_CREATED;
    gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
    ...
    frame = tstate->current_frame = prev;
    LOAD_IP(frame->return_offset);
}

_Py_MakeCoro already picked PyCoro_Type because of CO_COROUTINE, so the returned object is a coroutine. The frame is copied into gen->gi_iframe and ownership transfers to the generator; the original frame is popped and execution returns to the caller with the coroutine object on the stack.

The awaitable protocol

A coroutine is not an iterator on its own: PyCoro_Type sets tp_iter = 0 and tp_iternext = 0. You cannot next() it directly. Instead it implements the awaitable protocol through tp_as_async:

c
// Objects/genobject.c
static PyAsyncMethods coro_as_async = {
    coro_await,                                 /* am_await */
    0,                                          /* am_aiter */
    0,                                          /* am_anext */
    PyGen_am_send,                              /* am_send  */
};

static PyObject *
coro_await(PyObject *coro)
{
    PyCoroWrapper *cw = PyObject_GC_New(PyCoroWrapper, &_PyCoroWrapper_Type);
    if (cw == NULL) {
        return NULL;
    }
    cw->cw_coroutine = (PyCoroObject*)Py_NewRef(coro);
    _PyObject_GC_TRACK(cw);
    return (PyObject *)cw;
}

am_await returns a PyCoroWrapper, a small GC object holding a reference to the coroutine. The wrapper is an iterator (tp_iter = PyObject_SelfIter, tp_iternext = coro_wrapper_iternext) and delegates send/throw/close to the underlying coroutine:

c
// Objects/genobject.c
static PyObject *
coro_wrapper_iternext(PyObject *self)
{
    PyCoroWrapper *cw = _PyCoroWrapper_CAST(self);
    return gen_iternext((PyObject *)cw->cw_coroutine);
}

static PyObject *
coro_wrapper_send(PyObject *self, PyObject *arg)
{
    PyCoroWrapper *cw = _PyCoroWrapper_CAST(self);
    return gen_send((PyObject *)cw->cw_coroutine, arg);
}

am_send (used by await expressions and the event loop) calls gen_send_ex directly on the coroutine, bypassing the wrapper. This is what lets await coro drive it without allocating a wrapper in the hot path.

_PyCoro_GetAwaitableIter is the helper used by GET_AWAITABLE/YIELD_FROM to obtain something iterable from an arbitrary awaitable. For a coroutine it returns the object unchanged; for other awaitables it calls __await__ and validates that the result is an iterator, never another coroutine:

c
// Objects/genobject.c
PyObject *_PyCoro_GetAwaitableIter(PyObject *o)
{
    ...
    if (PyCoro_CheckExact(o) || gen_is_coroutine(o)) {
        /* 'o' is a coroutine. */
        return Py_NewRef(o);
    }
    ot = Py_TYPE(o);
    if (ot->tp_as_async != NULL) {
        getter = ot->tp_as_async->am_await;
    }
    if (getter != NULL) {
        PyObject *res = (*getter)(o);
        if (res != NULL) {
            if (PyCoro_CheckExact(res) || gen_is_coroutine(res)) {
                PyErr_Format(PyExc_TypeError,
                             "%T.__await__() must return an iterator, "
                             "not coroutine", o);
                Py_CLEAR(res);
            } else if (!PyIter_Check(res)) { ... }
        }
        return res;
    }
    ...
}

gen_is_coroutine also covers iterable coroutines (a plain generator whose code has CO_ITERABLE_COROUTINE), so types.coroutine-wrapped generators are accepted wherever a coroutine is.

cr_await

While a coroutine is suspended inside await other, cr_await exposes the object being awaited (or None). It is implemented by the same helper as gi_yieldfrom/ag_await: when the frame state is FRAME_SUSPENDED_YIELD_FROM, peek two slots down on the interpreter stack:

c
// Objects/genobject.c
static PyObject *
gen_getyieldfrom(PyObject *self, void *Py_UNUSED(ignored))
{
    PyGenObject *gen = _PyGen_CAST(self);
    ...
    int8_t frame_state = gen->gi_frame_state;
    if (frame_state != FRAME_SUSPENDED_YIELD_FROM) {
        Py_RETURN_NONE;
    }
    return PyStackRef_AsPyObjectNew(_PyFrame_StackPeek(&gen->gi_iframe, 2));
}

static PyGetSetDef coro_getsetlist[] = {
    ...
    {"cr_await", gen_getyieldfrom, NULL,
     PyDoc_STR("object being awaited on, or None")},
    ...
};

State, origin tracking, and finalization

Frame states are shared with generators (FRAME_CREATED, FRAME_SUSPENDED, FRAME_EXECUTING, FRAME_CLEARED). cr_state maps them to the strings CORO_CREATED, CORO_SUSPENDED, CORO_RUNNING, CORO_CLOSED. cr_running is true only in FRAME_EXECUTING.

Origin tracking stores a traceback-derived origin object in cr_origin_or_finalizer when sys.set_coroutine_origin_tracking_depth is non-zero; it is cleared on deallocation.

Finalization has coroutine-specific behaviour: if a coroutine is destroyed while still FRAME_CREATED (never awaited), CPython emits a RuntimeWarning: coroutine ... was never awaited; otherwise it closes it like a generator. The same check appears in _PyGen_ClearFrame:

c
// Objects/genobject.c (gen_finalize / _PyGen_ClearFrame excerpt)
if (_PyGen_GetCode(gen)->co_flags & CO_COROUTINE &&
    gen->gi_frame_state == FRAME_CREATED)
{
    _PyErr_WarnUnawaitedCoroutine((PyObject *)gen);
}

Deallocation clears cr_origin_or_finalizer for coroutine instances before releasing the frame, name, and qualname.

Key takeaways

  • There is no coroutineobject.c; coroutines are PyCoroObject in Objects/genobject.c, sharing _PyGenObject_HEAD(cr) with generators.
  • CO_COROUTINE on the code object selects PyCoro_Type in _Py_MakeCoro; CO_ITERABLE_COROUTINE keeps a generator that is still awaitable.
  • Calling an async def runs RETURN_GENERATOR, which copies the current frame into cr_iframe and returns the coroutine.
  • Coroutines are not iterators: am_await produces a PyCoroWrapper iterator, while am_send drives the coroutine directly.
  • _PyCoro_GetAwaitableIter returns a coroutine unchanged and validates that __await__ results are iterators, not coroutines.
  • cr_await peeks the suspended frame’s stack to report the awaited object.
  • Never-awaited coroutines raise RuntimeWarning on finalization/clearing.

Further reading

  • Objects/genobject.c – _Py_MakeCoro, PyCoro_Type, coro_await, _PyCoro_GetAwaitableIter, gen_getyieldfrom.
  • Include/cpython/genobject.h – PyCoroObject, PyCoro_Type.
  • Include/internal/pycore_interpframe_structs.h – _PyGenObject_HEAD.
  • Include/cpython/code.h – CO_COROUTINE and related flags.
  • Python/bytecodes.c – RETURN_GENERATOR.

PythonCpythonInternalsCoroutine