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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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 arePyCoroObjectinObjects/genobject.c, sharing_PyGenObject_HEAD(cr)with generators. CO_COROUTINEon the code object selectsPyCoro_Typein_Py_MakeCoro;CO_ITERABLE_COROUTINEkeeps a generator that is still awaitable.- Calling an
async defrunsRETURN_GENERATOR, which copies the current frame intocr_iframeand returns the coroutine. - Coroutines are not iterators:
am_awaitproduces aPyCoroWrapperiterator, whileam_senddrives the coroutine directly. _PyCoro_GetAwaitableIterreturns a coroutine unchanged and validates that__await__results are iterators, not coroutines.cr_awaitpeeks the suspended frame’s stack to report the awaited object.- Never-awaited coroutines raise
RuntimeWarningon 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_COROUTINEand related flags.Python/bytecodes.c–RETURN_GENERATOR.