Skip to main content

pystate.h (cpython/) — thread and interpreter state

Include/cpython/pystate.h is the CPython-extension tier of the thread-state API. The fully public Python.h exposes only opaque pointers and a handful of accessors; this header exposes the real struct layouts for embedders and CPython internals that need to read fields directly. It is included automatically when Py_LIMITED_API is not defined.

Map

LinesSymbolRole
1–50_PyErr_StackItemOne frame in the nested exception save/restore stack
51–90PyThreadState (frame / depth)Current frame pointer, recursion limit, and call depth
91–140PyThreadState (tracing)c_tracefunc, c_profilefunc, and their void* arguments
141–190PyThreadState (exception)current_exception, exc_info, and the _PyErr_StackItem head
191–230PyThreadState (async hooks)async_gen_firstiter, async_gen_finalizer callbacks
231–280PyInterpreterState pointerForward-declared struct and interp field in thread state
281–320_PyThreadState_GET / _PyInterpreterState_GETInline fast accessors via _PyRuntime
321–350GIL and ceval signalseval_breaker, gilstate_counter, pending calls flag

Reading

Nested exception state

Python allows except clauses to be re-entered and exceptions to be replaced. Each such nesting level is one _PyErr_StackItem pushed onto a singly-linked list hanging off the thread state.

// CPython: Include/cpython/pystate.h:28 _PyErr_StackItem
typedef struct _PyErr_StackItem {
PyObject *exc_value;
struct _PyErr_StackItem *previous_item;
} _PyErr_StackItem;

PyThreadState.exc_info points to the top of this stack. _PyErr_StackPush and _PyErr_StackPop manage it around except block entry and exit. The field was renamed from exc_traceback/exc_value/exc_type triple to a single normalised exc_value in 3.12.

Frame pointer and recursion depth

The thread state keeps a direct pointer to the currently executing frame. This pointer moves on every function call and return; it is the primary link between the eval loop and the object layer.

// CPython: Include/cpython/pystate.h:68 PyThreadState frame fields
struct _PyThreadState {
struct _PyInterpreterFrame *current_frame;
int recursion_depth;
int recursion_limit;
int recursion_headroom; /* space reserved for error recovery */
/* ... */
};

recursion_headroom reserves a small number of extra frames so that RecursionError can itself be constructed and raised without hitting a hard crash. The interpreter checks recursion_depth >= recursion_limit - recursion_headroom at each call site.

Tracing and profiling hooks

c_tracefunc and c_profilefunc are C-level function pointers set by sys.settrace and sys.setprofile respectively. When either is non-NULL the eval loop calls them at the corresponding event points. The companion void* fields carry the user-supplied arg passed to PyEval_SetTrace.

// CPython: Include/cpython/pystate.h:104 tracing fields
Py_tracefunc c_tracefunc;
Py_tracefunc c_profilefunc;
PyObject *c_traceobj;
PyObject *c_profileobj;
int tracing; /* non-zero while inside a trace call */

The tracing flag prevents re-entrant trace calls: if the trace function itself triggers Python execution, the inner frames are not traced.

Async generator hooks

Two optional callbacks let embedders intercept async generator lifecycle events. async_gen_firstiter is called the first time __anext__ is invoked on a freshly created async generator. async_gen_finalizer is called when the generator is about to be garbage-collected with a non-exhausted iterator.

// CPython: Include/cpython/pystate.h:200 async gen hooks
PyObject *async_gen_firstiter;
PyObject *async_gen_finalizer;

sys.set_asyncgen_hooks writes these fields. They default to NULL, in which case the interpreter skips the calls entirely. The hook mechanism was added in 3.6 alongside PEP 525.

Fast thread-state accessors

The two inline functions below are the hot path for every piece of CPython that needs the current thread or interpreter. They read directly from the _PyRuntime global rather than going through a TLS lookup on every call.

// CPython: Include/cpython/pystate.h:293 _PyThreadState_GET
static inline PyThreadState *
_PyThreadState_GET(void) {
return _PyRuntime.gilstate.tstate_current;
}

// CPython: Include/cpython/pystate.h:299 _PyInterpreterState_GET
static inline PyInterpreterState *
_PyInterpreterState_GET(void) {
return _PyThreadState_GET()->interp;
}

On free-threaded builds (3.13+) tstate_current becomes a thread-local variable rather than a single pointer in _PyRuntime.

gopy notes

gopy maps PyThreadState to vm.ThreadState in vm/eval_gen.go. The current_frame pointer corresponds to the frame field on the Go struct. Recursion depth tracking follows CPython's model with RecursionDepth and RecursionLimit fields. The c_tracefunc/c_profilefunc pair is not yet ported; sys.settrace is stubbed. The _PyErr_StackItem linked list is approximated by a Go slice in objects/instance.go that grows and shrinks around except block entry. _PyThreadState_GET has no direct equivalent because gopy passes the thread state explicitly through most call paths rather than relying on a global.

CPython 3.14 changes

3.14 continues the free-threaded work from 3.13. gilstate_counter is replaced by per-thread reference-count buffers in the no-GIL build. eval_breaker became a per-interpreter atomic rather than a field on each thread state, reducing cache pressure in multi-threaded programs. The _PyErr_StackItem layout is unchanged. recursion_headroom was introduced in 3.11 and has not changed since.