Skip to main content

Python/typevarobject.c

cpython 3.14 @ ab2d84fe1023/Python/typevarobject.c

PEP 695 type parameter objects. Before 3.12, TypeVar, TypeVarTuple, and ParamSpec were defined in pure Python inside typing.py. PEP 695 promoted them to C-level objects in typevarobject.c, giving them a stable memory layout and making them available at compile time for the new type X[T] = ... and def f[T]() syntax.

The file contains four independent type implementations that share almost no code: TypeVar (the classic single-type parameter), TypeVarTuple (a variadic type parameter that matches a variable number of types, written *Ts in annotations), ParamSpec (models the parameter list of a callable), and TypeAliasType (the object produced by a type X = Y statement). A small block of shared machinery at the bottom handles the __type_params__ attribute and generic parameter deduplication used by all four.

TypeAliasType is the most interesting of the four for an interpreter port because its __value__ field uses lazy evaluation via a PEP 696 callback: the right-hand side of a type statement is stored as a thunk and called on first access. TypeVar.__bound__ and TypeVar.__constraints__ support the same lazy evaluation pattern so that forward-referenced types in bounds do not need to be resolved at class-body parse time.

Map​

LinesSymbolRolegopy
1-300typevar_new, typevar_repr, typevar_getset, TypeVarObjectTypeVar struct layout, constructor from type[T] desugaring, __name__, __bound__, __constraints__, __covariant__, __contravariant__, __infer_variance__ getsets.objects/typevar.go:TypeVar, NewTypeVar
300-600typevartuple_new, typevartuple_repr, typevartuple_iter, TypeVarTupleObjectTypeVarTuple struct, constructor, __name__, __unpacked__ (the *Ts form), and the __iter__ that yields the unpacked form for use in subscriptions.objects/typevar.go:TypeVarTuple, NewTypeVarTuple
600-900paramspec_new, paramspec_repr, paramspec_args, paramspec_kwargs, ParamSpecObjectParamSpec struct, __name__, __bound__, __covariant__, __contravariant__, .args and .kwargs synthetic views.objects/typevar.go:ParamSpec, NewParamSpec
900-1200typealias_new, typealias_repr, typealias_getset, TypeAliasObjectTypeAliasType struct, constructor called by BUILD_TYPE_ALIAS opcode, __name__, __type_params__, __value__ with lazy thunk evaluation, __module__.objects/typevar.go:TypeAlias, NewTypeAlias
1200-1800_Py_make_type_params, collect_type_params, _Py_typing_type_or, _PyTypeVar_Type, _PyTypeVarTuple_Type, _PyParamSpec_Type, _PyTypeAliasType_TypeShared machinery: collect free type parameters from an annotation expression, wire __or__ / __ror__ on all four types to produce Union objects, and the four PyTypeObject definitions.objects/typevar.go:collectTypeParams, TypeVarType, TypeVarTupleType, ParamSpecType, TypeAliasType

Reading​

TypeVar layout and typevar_new (lines 1 to 300)​

cpython 3.14 @ ab2d84fe1023/Python/typevarobject.c#L1-300

The TypeVarObject struct carries eight fields: tv_name (a Unicode object), tv_bound (an object or a callable thunk), tv_evaluate_bound (the thunk itself so evaluate_bound() can be called explicitly), tv_constraints (a tuple), tv_evaluate_constraints, tv_default, tv_evaluate_default, and three char flags for covariance, contravariance, and infer-variance.

typevar_new is called directly by the MAKE_TYPEVAR opcode emitted for a type[T] binder. It parses keyword arguments, copies the name, and stores bound and constraints as either their resolved value or the thunk, depending on whether the caller supplied a callable:

static PyObject *
typevar_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"name", "bound", "constraints",
"covariant", "contravariant",
"infer_variance", "default", NULL};
PyObject *name, *bound = Py_None, *constraints = NULL;
PyObject *def = Py_None;
int covariant = 0, contravariant = 0, infer_variance = 0;

if (!PyArg_ParseTupleAndKeywords(args, kwds,
"U|OO!pppO:TypeVar", kwlist,
&name, &bound, &PyTuple_Type, &constraints,
&covariant, &contravariant, &infer_variance, &def))
return NULL;

TypeVarObject *tv = PyObject_GC_New(TypeVarObject, type);
if (tv == NULL) return NULL;
tv->tv_name = Py_NewRef(name);
tv->tv_bound = Py_XNewRef(bound);
tv->tv_evaluate_bound = NULL;
tv->tv_constraints = constraints ? Py_NewRef(constraints)
: PyTuple_New(0);
/* ... flags ... */
_PyObject_GC_TRACK(tv);
return (PyObject *)tv;
}

The __bound__ getset triggers lazy evaluation when tv_bound is a callable rather than a resolved type. The first call to the getter calls the thunk, caches the result into tv_bound, and then returns it. Subsequent accesses return the cached value directly. evaluate_bound() exposes the thunk call as an explicit public method so tools like typing.get_type_hints can force evaluation without going through attribute access.

TypeAliasType and lazy __value__ (lines 900 to 1200)​

cpython 3.14 @ ab2d84fe1023/Python/typevarobject.c#L900-1200

TypeAliasType is simpler than TypeVar in most respects but has one important twist: the __value__ field is always lazy. The type X = expr statement desugars to a BUILD_TYPE_ALIAS opcode that wraps expr in a zero-argument function before calling typealias_new. The struct stores both the thunk and the resolved value:

typedef struct {
PyObject_HEAD
PyObject *ta_name;
PyObject *ta_type_params; /* tuple of TypeVar/TypeVarTuple/ParamSpec */
PyObject *ta_value; /* resolved, or NULL if not yet called */
PyObject *ta_value_compute; /* the thunk, kept for re-evaluation */
PyObject *ta_module;
} TypeAliasObject;

static PyObject *
typealias_value(PyObject *self, void *unused)
{
TypeAliasObject *ta = (TypeAliasObject *)self;
if (ta->ta_value == NULL) {
ta->ta_value = PyObject_CallNoArgs(ta->ta_value_compute);
if (ta->ta_value == NULL)
return NULL;
}
return Py_NewRef(ta->ta_value);
}

This lazy evaluation means that a type X = SomeClass statement at module top level does not require SomeClass to exist yet. The value is computed only on the first X.__value__ access, by which time the module body has finished executing and all names are bound.

TypeAliasType also defines __getitem__ so that X[int] works for generic aliases. The subscription delegates to Py_GenericAlias(self, args), making type Alias[T] = list[T] subscriptable at runtime as Alias[int].

__type_params__ and PEP 695 generic tracking (lines 1200 to 1800)​

cpython 3.14 @ ab2d84fe1023/Python/typevarobject.c#L1200-1800

_Py_make_type_params is called during function and class definition for any PEP 695 generic. It receives the __type_params__ tuple that the compiler gathered while processing the [T, *Ts, **P] binder. The tuple is attached to the function or class via a __type_params__ attribute. The compiler emits COPY_FREE_VARS followed by LOAD_FAST "__type_params__" at the start of a generic function body to make the parameter tuple available inside the body scope.

collect_type_params is the inverse operation. Given an arbitrary annotation expression, it walks the object graph collecting any object whose type exposes __typing_subst__ (the interface that TypeVar, TypeVarTuple, and ParamSpec all implement). The result is the __parameters__ attribute value on generic aliases:

static int
collect_type_params(PyObject *obj, PyObject *params)
{
/* Base case: obj is itself a type parameter. */
if (PyObject_HasAttr(obj, &_Py_ID(__typing_subst__))) {
if (PyList_Append(params, obj) < 0)
return -1;
return 0;
}
/* Recurse into __args__ if present (nested generic alias). */
PyObject *args = PyObject_GetAttr(obj, &_Py_ID(__args__));
if (args == NULL) {
PyErr_Clear();
return 0;
}
Py_ssize_t n = PySequence_Length(args);
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *arg = PySequence_GetItem(args, i);
if (arg == NULL || collect_type_params(arg, params) < 0) {
Py_XDECREF(arg);
Py_DECREF(args);
return -1;
}
Py_DECREF(arg);
}
Py_DECREF(args);
return 0;
}

_Py_typing_type_or wires __or__ on all four type parameter types so that TypeVar | None produces a PyUnionObject rather than raising TypeError. The four PyTypeObject singletons (_PyTypeVar_Type, _PyTypeVarTuple_Type, _PyParamSpec_Type, _PyTypeAliasType_Type) are defined with GC support because they can form reference cycles through their thunk closures.

gopy mirror​

objects/typevar.go for TypeVar, TypeVarTuple, ParamSpec, TypeAlias, and the four type singletons TypeVarType, TypeVarTupleType, ParamSpecType, TypeAliasType. The lazy evaluation pattern (ta_value_compute thunk) is reproduced as a Go function field that is called on first access and then replaced with a static value. collectTypeParams corresponds directly to the C collect_type_params recursive walker.

CPython 3.14 changes​

PEP 695 (type parameter syntax, TypeAliasType) shipped in 3.12. The file was created at that point by migrating the three typing.py classes to C. TypeVar.__default__ and evaluate_default() (PEP 696 defaults for type parameters) were added in 3.13. TypeVarTuple.__unpacked__ (the *Ts view) and its __iter__ were also added in 3.13. The 3.14 additions are minor: __typing_subst__ is now a slot on the type object rather than an attribute lookup, speeding up collect_type_params traversal.