Skip to main content

Python/context.c

cpython 3.14 @ ab2d84fe1023/Python/context.c

Context variable machinery introduced by PEP 567. Three public types work together: PyContext is an immutable snapshot of a variable-to-value mapping, PyContextVar is a typed key that carries a name and an optional default value, and PyContextToken is the handle returned by PyContextVar_Set that lets a caller undo a set via PyContextVar_Reset.

The mapping inside every PyContext is a Hash Array Mapped Trie (HAMT, see hamt.c). Because the HAMT is persistent, installing a new value produces a new root node while leaving all old roots intact. This is what makes ctx.run(callable) safe: the interpreter pushes a copy, runs the callable, and pops back to the original without disturbing any concurrent threads or nested calls that held a reference to the old context.

The interpreter's thread state keeps a tstate->context pointer to the currently active PyContext. PyContext_Enter / PyContext_Exit push and pop that pointer using a linked list of PyContextObject nodes rather than a separate stack structure, because the PyContextObject itself stores the previous context in ctx_prev_context.

Map​

LinesSymbolRolegopy
1-80PyContext type object, context_new / context_deallocAllocate and free a PyContextObject; the HAMT root lives in ctx_vars.module/contextlib/module.go:newContext
81-200PyContext_CopyCurrentCopy the interpreter's current context for use in a new thread; shares the immutable HAMT root safely.module/contextlib/module.go:CopyCurrent
201-320PyContext_Enter / PyContext_ExitPush/pop tstate->context; Enter saves the previous context in ctx_prev_context.module/contextlib/module.go:Enter / Exit
321-480PyContextVar type object, contextvar_newAllocate a PyContextVar; store name and optional default in var_name / var_default.module/contextlib/module.go:newContextVar
481-600PyContextVar_GetLook up the variable in the current context's HAMT; return the default or raise LookupError when absent.module/contextlib/module.go:ContextVarGet
601-760PyContextVar_SetCall _PyHamt_Assoc to produce a new HAMT root; wrap the old value in a PyContextToken; update tstate->context.module/contextlib/module.go:ContextVarSet
761-880PyContextVar_ResetValidate the token belongs to this variable and the current context; restore the old value via _PyHamt_Assoc or _PyHamt_Without.module/contextlib/module.go:ContextVarReset
881-1000PyContextToken type object, token_new / token_deallocImmutable record of (var, old_value, context_at_set); used flag prevents double-reset.module/contextlib/module.go:newContextToken

Reading​

PyContext over HAMT (lines 1 to 200)​

cpython 3.14 @ ab2d84fe1023/Python/context.c#L1-200

typedef struct {
PyObject_HEAD
PyObject *ctx_vars; /* PyHamtObject * */
PyObject *ctx_prev_context;
uint64_t ctx_run_ref_count;
Py_hash_t ctx_cached_hash;
} PyContextObject;

The ctx_vars field is a PyHamtObject * — the persistent trie from hamt.c. Because the HAMT never mutates in place, two PyContextObject instances can share sub-tries with zero copying; only the root pointer differs. ctx_prev_context threads the stack that PyContext_Enter / PyContext_Exit manipulates.

PyContext_CopyCurrent (lines 81 to 200) copies the thread-local context for handoff to a new thread. The implementation calls _PyHamt_Copy on the current ctx_vars, which in practice just increments the root's reference count because HAMTs are immutable.

PyObject *
PyContext_CopyCurrent(void)
{
PyThreadState *ts = _PyThreadState_GET();
PyObject *ctx = ts->context;
if (ctx == NULL) {
return context_new_empty();
}
return context_new_from_vars(
((PyContextObject *)ctx)->ctx_vars);
}

PyContextVar_Set and PyContextToken (lines 601 to 880)​

cpython 3.14 @ ab2d84fe1023/Python/context.c#L601-880

PyContextVar_Set is the hot path for var.set(value). It calls _PyHamt_Assoc to get a new HAMT root, then allocates a PyContextToken recording the old value, and installs a fresh PyContextObject onto tstate->context.

PyObject *
PyContextVar_Set(PyObject *ovar, PyObject *val)
{
PyContextVar *var = (PyContextVar *)ovar;
PyContextObject *ctx = get_thread_context();

PyObject *old_val = NULL;
if (_PyHamt_Find(ctx->ctx_vars, var->var_key, &old_val) == -1) {
return NULL;
}

PyObject *new_vars = _PyHamt_Assoc(ctx->ctx_vars, var->var_key, val);
if (new_vars == NULL) {
return NULL;
}

PyObject *tok = token_new(ctx, var, old_val);
...
ctx = context_new_from_vars(new_vars);
...
set_thread_context(ctx);
return tok;
}

PyContextVar_Reset (lines 761 to 880) validates the token's used flag, confirms that the current context matches token->tok_ctx, then either calls _PyHamt_Assoc with the saved old value or _PyHamt_Without if the variable was absent when the token was created.

PyContext_Enter and PyContext_Exit (lines 201 to 320)​

cpython 3.14 @ ab2d84fe1023/Python/context.c#L201-320

int
PyContext_Enter(PyObject *octx)
{
PyContextObject *ctx = (PyContextObject *)octx;
PyThreadState *ts = _PyThreadState_GET();
ctx->ctx_prev_context = ts->context;
Py_XINCREF(ctx->ctx_prev_context);
ts->context = (PyObject *)ctx;
Py_INCREF(ctx);
ctx->ctx_run_ref_count++;
return 0;
}

int
PyContext_Exit(PyObject *octx)
{
PyContextObject *ctx = (PyContextObject *)octx;
PyThreadState *ts = _PyThreadState_GET();
...
ts->context = ctx->ctx_prev_context;
ctx->ctx_prev_context = NULL;
ctx->ctx_run_ref_count--;
Py_DECREF(ctx);
return 0;
}

ctx_run_ref_count guards against a context being entered on two threads or reentered on one; attempting to enter an already-active context raises RuntimeError. The pop path restores ts->context to whatever was saved in ctx_prev_context at enter time, so nested run() calls correctly unwind.

Notes for the gopy mirror​

module/contextlib/module.go ports all three public types. The HAMT calls (_PyHamt_Assoc, _PyHamt_Without, _PyHamt_Find) map to the methods on objects/hamt.go:Hamt. The thread-state pointer (tstate->context) is replaced by a per-goroutine value stored in the interpreter's ThreadState struct.

CPython 3.14 changes worth noting​

The PyContextVar and PyContext APIs are stable since 3.7. In 3.14 the only notable change is alignment with the free-threaded build (PEP 703): ctx_run_ref_count uses atomic operations in the free-threaded build, but the GIL-holding path is unchanged.