How CPython Implements Generator (types.GeneratorType) in C
PyGenObject and its embedded _PyInterpreterFrame, gi_frame_state, RETURN_GENERATOR, gen_send_ex2, throw/close, and clearing traced through the CPython 3.15 source.
A Python generator is, at the C level, a single object that is its own
frame: the executable state of a paused function lives inside the object, so a
generator can be suspended, resumed, thrown into, and closed from anywhere.
In CPython 3.15 generators, coroutines and async generators share one
implementation in Objects/genobject.c and one struct layout defined by
_PyGenObject_HEAD.
This article traces that implementation.
The struct: _PyGenObject
The head macro is shared by all three flavours:
// 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 _PyGenObject { _PyGenObject_HEAD(gi) };
struct _PyCoroObject { _PyGenObject_HEAD(cr) };
struct _PyAsyncGenObject { _PyGenObject_HEAD(ag) };So a PyGenObject is just PyObject_HEAD plus a weakref list, name/qualname,
the saved exception state, a finalizer/origin pointer, a few flags, the
frame state byte gi_frame_state, and an embedded _PyInterpreterFrame
gi_iframe. The frame has owner == FRAME_OWNED_BY_GENERATOR, which is how
_PyGen_GetGeneratorFromFrame recovers the owning generator from a frame
pointer (it subtracts offsetof(PyGenObject, gi_iframe)).
Because the frame is embedded, a generator is one allocation: the object is the activation record. There is no separate stack frame to keep alive.
Creating a generator: RETURN_GENERATOR
A generator function is detected at compile time (CO_GENERATOR). When it is
called, the interpreter runs the function body until the first RETURN_GENERATOR
bytecode, which converts the in-flight frame into a generator object:
// Python/bytecodes.c
inst(RETURN_GENERATOR, (-- res)) {
PyFunctionObject *func =
(PyFunctionObject *)PyStackRef_AsPyObjectBorrow(frame->f_funcobj);
PyGenObject *gen = (PyGenObject *)_Py_MakeCoro(func);
ERROR_IF(gen == NULL);
assert(STACK_LEVEL() <= 2);
SAVE_STACK();
_PyInterpreterFrame *gen_frame = &gen->gi_iframe;
frame->instr_ptr++;
_PyFrame_Copy(frame, gen_frame); // copy locals + value stack
assert(frame->frame_obj == NULL);
gen->gi_frame_state = FRAME_CREATED;
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *prev = frame->previous;
_PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
_PyThreadState_PopFrame(tstate, frame);
frame = tstate->current_frame = prev;
LOAD_IP(frame->return_offset);
RELOAD_STACK();
res = PyStackRef_FromPyObjectStealMortal((PyObject *)gen);
...
}_Py_MakeCoro allocates the object (PyObject_GC_NewVar, sized for the
frame’s locals+stack slots) and fills in gi_name/gi_qualname from the
function. RETURN_GENERATOR then copies the current thread frame into
gi_iframe with _PyFrame_Copy, sets gi_frame_state = FRAME_CREATED, and
pops the original frame off the thread stack. From that point the generator
owns its own frame; the caller just receives the generator object.
_PyFrame_Copy is worth a look because it is what makes “pausing” work: it
copies the executable, globals, builtins, locals, instr_ptr, and the entire
value stack into the embedded frame, and converts stack references to
heap-safe ones (the generator may outlive the thread stack):
// Include/internal/pycore_interpframe.h
static inline void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *dest)
{
dest->f_executable = PyStackRef_MakeHeapSafe(src->f_executable);
dest->previous = NULL; // no dangling pointer to old frame
dest->f_funcobj = PyStackRef_MakeHeapSafe(src->f_funcobj);
dest->f_globals = src->f_globals;
dest->f_builtins = src->f_builtins;
dest->f_locals = src->f_locals;
dest->frame_obj = src->frame_obj;
dest->instr_ptr = src->instr_ptr;
...
for (int i = 0; i < stacktop; i++) {
dest->localsplus[i] = PyStackRef_MakeHeapSafe(src->localsplus[i]);
}
}Generator state
gi_frame_state is an int8_t holding one of:
// Include/internal/pycore_frame.h
typedef enum _framestate {
FRAME_CREATED = 0,
FRAME_SUSPENDED = 1,
FRAME_SUSPENDED_YIELD_FROM = 2,
FRAME_SUSPENDED_YIELD_FROM_LOCKED = 3,
FRAME_EXECUTING = 4,
FRAME_CLEARED = 5
} PyFrameState;
#define FRAME_STATE_SUSPENDED(S) ((S) >= FRAME_SUSPENDED && (S) <= FRAME_SUSPENDED_YIELD_FROM_LOCKED)
#define FRAME_STATE_FINISHED(S) ((S) == FRAME_CLEARED)Exposed to Python as gi_state (GEN_CREATED, GEN_SUSPENDED, GEN_RUNNING,
GEN_CLOSED). All state transitions use atomic loads/stores and
_Py_GEN_TRY_SET_FRAME_STATE, which is necessary for the free-threaded build.
gi_code and gi_frame
gi_code is not a stored field; it is derived from the embedded frame:
// Objects/genobject.c
static inline PyCodeObject *_PyGen_GetCode(PyGenObject *gen) {
return _PyFrame_GetCode(&gen->gi_iframe); // (PyCodeObject*)gi_iframe.f_executable
}
static PyObject *gen_getcode(PyObject *self, void *ignored) {
PyGenObject *gen = _PyGen_CAST(self);
return _gen_getcode(gen, "gi_code"); // Py_NewRef(_PyGen_GetCode(gen))
}gi_frame is similarly lazy: _gen_getframe returns _PyFrame_GetFrameObject(&gen->gi_iframe)
unless the generator is finished, in which case it returns None. The frame
object is created on demand and cached in gi_iframe.frame_obj.
send(): gen_send_ex / gen_send_ex2
The public send()/__next__() path goes through gen_send_ex, which
validates state and atomically moves CREATED/SUSPENDED -> EXECUTING,
then calls gen_send_ex2:
// Objects/genobject.c
static PySendResult
gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult, int exc)
{
assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_EXECUTING);
PyThreadState *tstate = _PyThreadState_GET();
_PyInterpreterFrame *frame = &gen->gi_iframe;
/* Push arg onto the frame's value stack */
PyObject *arg_obj = arg ? arg : Py_None;
_PyFrame_StackPush(frame, PyStackRef_FromPyObjectNew(arg_obj));
_PyErr_StackItem *prev_exc_info = tstate->exc_info;
gen->gi_exc_state.previous_item = prev_exc_info;
tstate->exc_info = &gen->gi_exc_state;
if (exc) {
assert(_PyErr_Occurred(tstate));
_PyErr_ChainStackItem();
}
EVAL_CALL_STAT_INC(EVAL_CALL_GENERATOR);
PyObject *result = _PyEval_EvalFrame(tstate, frame, exc);
...
int return_kind = ((_PyThreadStateImpl *)tstate)->generator_return_kind;
if (return_kind == GENERATOR_YIELD) {
assert(result != NULL && !_PyErr_Occurred(tstate));
*presult = result;
return PYGEN_NEXT; // yielded
}
assert(return_kind == GENERATOR_RETURN);
...
*presult = result;
return result ? PYGEN_RETURN : PYGEN_ERROR; // exhausted / error
}Key points:
- The sent value is pushed onto the generator’s own value stack as the result
of the
yieldexpression, via_PyFrame_StackPush. - Exception state is swapped in:
tstate->exc_infopoints atgen->gi_exc_statefor the duration of the call, and restored afterwards. _PyEval_EvalFrameruns the embedded frame. When it hitsYIELD_VALUE, ceval setsgenerator_return_kind = GENERATOR_YIELDand returns the yielded object; the frame staysEXECUTINGuntilgen_send_ex2finishes, then transitions toSUSPENDED.- On exhaustion,
gi_frame_statebecomesFRAME_CLEAREDandgen_clear_framereleases locals.
gen_send_ex also guards against the obvious mistakes: sending a non-None
value into a just-created generator ("can't send non-None value to a just-started generator"), re-entering an already-running generator, and
reusing an exhausted coroutine.
throw()
_gen_throw reuses the same machinery. After validating state and moving to
EXECUTING, it handles yield from delegation: if the generator is suspended
inside a yield from, the thrown exception is forwarded to the sub-iterator
(yf), possibly recursively. Otherwise it lands at throw_here, where the
exception is restored into the current error indicator and gen_send_ex2 is
called with exc=1:
// Objects/genobject.c
throw_here:
assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_EXECUTING);
if (gen_set_exception(typ, val, tb) < 0) { ... }
return gen_throw_current_exception(gen); // -> gen_send_ex2(gen, Py_None, &result, 1)
gen_set_exception normalizes the (type, value, traceback) triple into the
current exception (calling PyErr_NormalizeException when given a class).
close()
gen_close is a state machine over gi_frame_state:
FRAME_CREATED: atomically move toFRAME_CLEARED, callgen_clear_frame, returnNone. The generator never ran.FRAME_CLEARED(finished): returnNone.FRAME_EXECUTING: raise “generator already executing”.- suspended: move to
EXECUTING, and if suspended inyield from, close the sub-iterator first. Then, if the resume point is a plainRESUMEafter a yield with exception depth 1, it can clear the frame directly; otherwise it injectsGeneratorExitand runs the generator to completion viagen_send_ex2(..., exc=1).
// Objects/genobject.c
if (err == 0) {
PyErr_SetNone(PyExc_GeneratorExit);
}
PyObject *retval;
if (gen_send_ex2(gen, Py_None, &retval, 1) == PYGEN_RETURN) {
// generator returned a value while closing
return retval;
} else if (retval) {
... PyErr_SetString(PyExc_RuntimeError, "generator ignored GeneratorExit");
} ...
if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
PyErr_Clear();
Py_RETURN_NONE;
}A generator that catches GeneratorExit and yields again produces
"generator ignored GeneratorExit".
Clearing and deallocation
gen_clear_frame runs once the state is FRAME_CLEARED:
// Objects/genobject.c
static void gen_clear_frame(PyGenObject *gen)
{
assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_CLEARED);
_PyInterpreterFrame *frame = &gen->gi_iframe;
_PyThreadState_UpdateLastProfiledFrame(_PyThreadState_GET(), frame, frame->previous);
frame->previous = NULL;
_PyFrame_ClearExceptCode(frame); // clear locals + value stack
_PyErr_ClearExcState(&gen->gi_exc_state);
}_PyFrame_ClearExceptCode clears locals and drops f_funcobj; if a
PyFrameObject is still referenced it takes ownership by copying the embedded
frame into the heap frame object (so the frame survives the generator).
gen_dealloc (called by the GC) makes sure the frame is cleared before freeing:
// Objects/genobject.c
if (gen->gi_frame_state != FRAME_CLEARED) {
gen->gi_frame_state = FRAME_CLEARED;
gen_clear_frame(gen);
}
assert(gen->gi_exc_state.exc_value == NULL);
PyStackRef_CLEAR(gen->gi_iframe.f_executable);
Py_CLEAR(gen->gi_name);
Py_CLEAR(gen->gi_qualname);
PyObject_GC_Del(gen);gen_traverse keeps the GC honest: while the frame is not cleared it traverses
gi_iframe (via _PyFrame_Traverse); when cleared it still visits the code
object held by f_executable so a deferred reference keeps it alive.
gen_finalize (tp_finalize) handles unawaited coroutines by issuing a
RuntimeWarning, and otherwise closes the generator. Async generators may
instead invoke their ag_origin_or_finalizer.
yield from support
When a generator is suspended inside yield from, gi_frame_state is
FRAME_SUSPENDED_YIELD_FROM and the sub-iterator sits on the value stack (slot
2 of localsplus-relative peek). gen_getyieldfrom reads it (locking the state
to ..._LOCKED on the free-threaded build to avoid races), and close()/
throw() use it to delegate to the sub-iterator.
Key takeaways
- A generator is one GC object whose payload includes an
_PyInterpreterFrame; it is created byRETURN_GENERATOR, which copies the in-flight thread frame into the object and pops the original. gi_frame_state(created / suspended / suspended-yield-from / executing / cleared) tracks execution, with atomic transitions for free-threading.gi_codeandgi_frameare derived fromgi_iframe, not stored fields.send()pushes the argument onto the generator’s value stack, swaps ingi_exc_state, runs_PyEval_EvalFrame, and classifies the result bygenerator_return_kind.throw()forwards into ayield fromsub-iterator or injects an exception and resumes.close()injectsGeneratorExit(or clears directly) and runs to completion.- Clearing releases locals and, if a frame object escaped, transfers ownership
via
take_ownership.
Further reading
Objects/genobject.c– all of the above.Python/bytecodes.c(RETURN_GENERATOR) – frame-to-generator conversion.Include/internal/pycore_interpframe_structs.h–_PyGenObject_HEAD.Include/internal/pycore_frame.h– frame state enum.Include/internal/pycore_interpframe.h–_PyFrame_Copy,_PyFrame_ClearExceptCode.Objects/frameobject.c–PyFrameObjectmaterialization.InternalDocs/frames.md– frame design notes.