Python/context.c
Python/context.c implements contextvars.Context, contextvars.ContextVar,
and contextvars.Token. Contexts are shallow-immutable HAMT (Hash Array Mapped
Trie) snapshots; entering a context installs it on the thread state stack and
exiting restores the previous one.
Map
| Lines | Symbol | Role |
|---|---|---|
| 1-60 | includes / forward decls | Internal HAMT API and _PyHamt type |
| 61-120 | PyContext struct | Wraps one _PyHamt *ctx_vars root |
| 121-200 | context_new / PyContext_New | Allocate a fresh empty context |
| 201-280 | PyContext_Copy | Return a new PyContext sharing the same HAMT root |
| 281-380 | PyContext_Enter / PyContext_Exit | Push/pop on tstate->context |
| 381-500 | PyContextVar struct + constructors | Name, default value, optional type check |
| 501-620 | PyContextVar_Get | HAMT lookup with fallback to default |
| 621-740 | PyContextVar_Set | HAMT assoc on current context |
| 741-820 | PyContextVar_Reset | Restore old value recorded in a Token |
| 821-920 | PyContext_Token struct | Stores var, old_value, used flag |
| 921-1000 | Type objects + module init | PyContext_Type, PyContextVar_Type, PyToken_Type |
Reading
PyContext_Copy: sharing the HAMT root
Copy is O(1) because HAMTs are persistent: copying is just bumping the
reference count on the existing root node.
// CPython: Python/context.c:215 PyContext_Copy
PyObject *
PyContext_Copy(PyObject *octx)
{
PyContext *ctx = (PyContext *)octx;
return (PyObject *)context_new(ctx->ctx_vars);
}
Two contexts share the same ctx_vars pointer until one of them calls
PyContextVar_Set, which produces a new HAMT root via assoc and stores it
only in the current context.
PyContext_Enter and PyContext_Exit
// CPython: Python/context.c:295 PyContext_Enter
int
PyContext_Enter(PyObject *octx)
{
PyContext *ctx = (PyContext *)octx;
PyThreadState *ts = _PyThreadState_GET();
ctx->ctx_prev = ts->context; /* save caller's context */
ts->context = (PyObject *)ctx;
Py_INCREF(ctx);
ts->context_ver++;
return 0;
}
// CPython: Python/context.c:330 PyContext_Exit
int
PyContext_Exit(PyObject *octx)
{
PyContext *ctx = (PyContext *)octx;
PyThreadState *ts = _PyThreadState_GET();
ts->context = ctx->ctx_prev;
ctx->ctx_prev = NULL;
Py_DECREF(ctx);
ts->context_ver++;
return 0;
}
ctx_prev forms a singly-linked stack. context_ver is a monotonic counter;
any cache keyed on context identity invalidates itself when context_ver
changes. PyContext_Exit does not call Py_DECREF(ctx->ctx_prev) because
ctx_prev is a borrowed reference to the caller's context.
PyContextVar_Get: HAMT lookup with default fallback
// CPython: Python/context.c:520 PyContextVar_Get
int
PyContextVar_Get(PyObject *ovar, PyObject *def, PyObject **val)
{
PyContextVar *var = (PyContextVar *)ovar;
PyThreadState *ts = _PyThreadState_GET();
PyContext *ctx = (PyContext *)ts->context;
PyObject *found = _PyHamt_Find(ctx->ctx_vars, (PyObject *)var);
if (found != NULL) {
Py_INCREF(found);
*val = found;
return 0;
}
/* fall back: explicit default arg, then var->var_default */
if (def != NULL) {
Py_INCREF(def);
*val = def;
} else if (var->var_default != NULL) {
Py_INCREF(var->var_default);
*val = var->var_default;
} else {
*val = NULL;
}
return 0;
}
The HAMT key is the PyContextVar * pointer itself, compared by identity.
This means two different ContextVar objects with the same name are always
distinct keys.
PyContextVar_Set and Token
// CPython: Python/context.c:650 PyContextVar_Set
PyObject *
PyContextVar_Set(PyObject *ovar, PyObject *val)
{
PyContextVar *var = (PyContextVar *)ovar;
PyThreadState *ts = _PyThreadState_GET();
PyContext *ctx = (PyContext *)ts->context;
/* snapshot old value for Token */
PyObject *old = _PyHamt_Find(ctx->ctx_vars, (PyObject *)var);
PyObject *new_vars = _PyHamt_Assoc(ctx->ctx_vars,
(PyObject *)var, val);
if (new_vars == NULL) {
return NULL;
}
Py_SETREF(ctx->ctx_vars, new_vars); /* mutate current context in place */
return context_token_new(ctx, var, old); /* returns a Token */
}
_PyHamt_Assoc is purely functional: it returns a new HAMT root with the
key/value added, leaving the original root intact (for contexts that share it
via Copy). The current context swaps in the new root via Py_SETREF.
gopy notes
PyContextmaps toobjects.ContextObjectin gopy.ctx_varsis*hamt.Nodefrom themodule/_hamtpackage.PyContext_Enter/PyContext_Exitcorrespond tovm.PushContext/vm.PopContextwhich manipulatetstate.Context.PyContextVar_Getmaps toobjects.ContextVar.Get; the three-way fallback (found / explicit default / var default) is reproduced exactly.Tokenisobjects.ContextToken; itsusedguard prevents double-reset.context_veris tracked aststate.ContextVer uint64and is incremented on every enter/exit, matching CPython's invalidation semantics.
CPython 3.14 changes
- 3.14 adds
PyContext_CopyCurrentas a C-API convenience that combinesPyContext_CopyCurrent = PyContext_Copy(PyContext_CopyCurrent())in one call, reducing boilerplate in extension modules (gh-116008). - The
ctx_weakreflistfield was added toPyContextin 3.14 to allow weak references to context objects (gh-119006). context_veroverflow handling was hardened: the counter now wraps atUINT64_MAXwith an explicit modular increment rather than undefined signed overflow (gh-112130).PyContextVar_Resetgains an early-exit whentoken->tok_usedis set, raisingRuntimeErroron double-reset. The check existed informally before but is now codified with a dedicated error message.