CPython's TypeVar, TypeVarTuple, and ParamSpec Implementation in C
TypeVar, TypeVarTuple, ParamSpec (and TypeAliasType) as C structs in Objects/typevarobject.c, their layouts and GC handling, the NoDefault marker, lazy evaluation via constevaluator, variance and defaults, ParamSpecArgs/Kwargs, and how the _typing module exposes them, traced through CPython 3.15.
Python’s type variables – typing.TypeVar, typing.TypeVarTuple,
typing.ParamSpec (plus typing.TypeAliasType) – used to live entirely in
Lib/typing.py. In CPython 3.15 they are implemented in C in
Objects/typevarobject.c and exposed through the _typing extension module.
This article traces that implementation.
Overview
typevarobject.c defines several related types in one file:
typevarobject->typing.TypeVartypevartupleobject->typing.TypeVarTupleparamspecobject->typing.ParamSpecparamspecattrobject->typing.ParamSpecArgs/ParamSpecKwargstypealiasobject->typing.TypeAliasType_ConstEvaluatorandNoDefaultType(internal helpers)
All of these are GC-tracked heap types created from PyType_Spec/PyType_Slot
tables and cached in interp->cached_objects during interpreter
initialization (_Py_initialize_generic).
Struct layouts
// Objects/typevarobject.c
typedef struct {
PyObject_HEAD
PyObject *name;
PyObject *bound;
PyObject *evaluate_bound;
PyObject *constraints;
PyObject *evaluate_constraints;
PyObject *default_value;
PyObject *evaluate_default;
bool covariant;
bool contravariant;
bool infer_variance;
} typevarobject;
typedef struct {
PyObject_HEAD
PyObject *name;
PyObject *bound;
PyObject *default_value;
PyObject *evaluate_default;
bool covariant;
bool contravariant;
bool infer_variance;
} typevartupleobject;
typedef struct {
PyObject_HEAD
PyObject *name;
PyObject *bound;
PyObject *default_value;
PyObject *evaluate_default;
bool covariant;
bool contravariant;
bool infer_variance;
} paramspecobject;Key differences:
TypeVaris the only one that supportsconstraints(a tuple of types) in addition tobound;TypeVarTupleandParamSpeconly havebound.- All three share variance flags (
covariant,contravariant,infer_variance) and an optionaldefault_value. ParamSpecArgs/ParamSpecKwargsare a separate, smaller struct (paramspecattrobject) holding a single__origin__pointer back to the ParamSpec they came from.
Lazy evaluation: evaluate_* fields
A modern feature (PEP 695 / deferred evaluation) is that bounds and defaults
can be stored as evaluator functions rather than concrete values. Each struct
carries both a resolved field (bound, default_value, constraints) and an
evaluate_* callable. The public getters resolve lazily:
// Objects/typevarobject.c
static PyObject *
typevar_bound(PyObject *op, void *Py_UNUSED(closure))
{
typevarobject *self = typevarobject_CAST(op);
if (self->bound != NULL) {
return Py_NewRef(self->bound);
}
if (self->evaluate_bound == NULL) {
Py_RETURN_NONE;
}
PyObject *bound = PyObject_CallNoArgs(self->evaluate_bound);
self->bound = Py_XNewRef(bound); // cache the result
return bound;
}The same pattern is used for __default__ and __constraints__. The
evaluate_bound/evaluate_default/evaluate_constraints accessors expose the
raw evaluator (or, if a value was given directly, wrap it in a
_ConstEvaluator):
static PyObject *
typevar_evaluate_bound(PyObject *op, void *Py_UNUSED(closure))
{
typevarobject *self = typevarobject_CAST(op);
if (self->evaluate_bound != NULL) {
return Py_NewRef(self->evaluate_bound);
}
if (self->bound != NULL) {
return constevaluator_alloc(self->bound);
}
Py_RETURN_NONE;
}_ConstEvaluator is an internal, non-instantiable, GC-tracked type whose
__call__ returns the stored value (or renders it as a string for
_Py_ANNOTATE_FORMAT_STRING). This lets the typing machinery treat
eagerly- and lazily-provided values uniformly.
The NoDefault marker
Defaults use a sentinel to distinguish “no default” from “default is None”:
// Objects/typevarobject.c
static PyObject *
NoDefault_repr(PyObject *op) { return PyUnicode_FromString("typing.NoDefault"); }
PyTypeObject _PyNoDefault_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"NoDefaultType",
.tp_dealloc = nodefault_dealloc, // re-immortalizes if decref'd
...
};
PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type);nodefault_dealloc re-sets the reference count to immortal rather than
freeing, so the singleton can never be destroyed. typevar_alloc stores
default_value via Py_XNewRef, and typevar_has_default checks both
evaluate_default != NULL and that default_value is neither NULL nor the
_Py_NoDefaultStruct singleton.
Construction
static typevarobject *
typevar_alloc(PyObject *name, PyObject *bound, PyObject *evaluate_bound,
PyObject *constraints, PyObject *evaluate_constraints,
PyObject *default_value,
bool covariant, bool contravariant, bool infer_variance,
PyObject *module)
{
PyTypeObject *tp = _PyInterpreterState_GET()->cached_objects.typevar_type;
typevarobject *tv = PyObject_GC_New(typevarobject, tp);
...
tv->name = Py_NewRef(name);
tv->bound = Py_XNewRef(bound);
...
tv->default_value = Py_XNewRef(default_value);
tv->evaluate_default = NULL;
_PyObject_GC_TRACK(tv);
if (module != NULL) {
PyObject_SetAttrString((PyObject *)tv, "__module__", module);
}
return tv;
}The public TypeVar.__new__ (clinic-generated typevar_new) validates
arguments:
covariantandcontravariantcannot both be true.infer_variancecannot be combined with explicit variance.boundis passed throughtype_check(which callstyping._type_check).- A single constraint is forbidden; zero constraints becomes
NULL; constraints andboundare mutually exclusive.
The caller’s module is captured from the current frame (caller()), so
__module__ is set automatically – this is what makes pickling work for
globals.
Variance and repr
static PyObject *
typevar_repr(PyObject *self)
{
typevarobject *tv = typevarobject_CAST(self);
if (tv->infer_variance) {
return Py_NewRef(tv->name); // variance inferred: plain name
}
char variance = tv->covariant ? '+' : tv->contravariant ? '-' : '~';
return PyUnicode_FromFormat("%c%U", variance, tv->name);
}Manual TypeVars are invariant by default (~), covariant (+), or
contravariant (-). When infer_variance=True, the repr drops the prefix
because the variance is determined by the type checker from usage.
GC and deallocation
Every struct is GC-tracked. Dealloc untracks, clears all owned references
(including the managed dict and weakrefs), then frees with
PyObject_GC_Del:
static void
typevar_dealloc(PyObject *self)
{
PyTypeObject *tp = Py_TYPE(self);
typevarobject *tv = typevarobject_CAST(self);
_PyObject_GC_UNTRACK(self);
Py_XDECREF(tv->name);
Py_XDECREF(tv->bound);
Py_XDECREF(tv->evaluate_bound);
...
PyObject_ClearManagedDict(self);
PyObject_ClearWeakRefs(self);
Py_TYPE(self)->tp_free(self);
Py_DECREF(tp);
}traverse visits Py_TYPE(self) plus all owned PyObject fields, and
clear does Py_CLEAR on them. The same pattern repeats for
typevartupleobject, paramspecobject, and paramspecattrobject.
ParamSpecArgs / ParamSpecKwargs
These are lightweight GC objects referencing their origin ParamSpec:
typedef struct {
PyObject_HEAD
PyObject *__origin__;
} paramspecattrobject;ParamSpec.args and ParamSpec.kwargs (getset descriptors) construct them
on demand:
static PyObject *
paramspec_args(PyObject *self, void *Py_UNUSED(closure))
{
PyTypeObject *tp = _PyInterpreterState_GET()->cached_objects.paramspecargs_type;
return (PyObject *)paramspecattr_new(tp, self);
}They compare by origin and render as P.args / P.kwargs when the origin is
a real ParamSpec, otherwise as repr(origin).args.
| (union) operator
TypeVar and ParamSpec support | via nb_or = make_union, which builds a
typing.Union (not types.UnionType) to preserve legacy forward-reference
string behavior:
static PyObject *
make_union(PyObject *self, PyObject *other)
{
PyObject *args = _PyTuple_FromPair(self, other);
PyObject *u = _Py_union_from_tuple(args);
Py_DECREF(args);
return u;
}Initialization and exposure
int _Py_initialize_generic(PyInterpreterState *interp)
{
MAKE_TYPE(generic);
MAKE_TYPE(typevar);
MAKE_TYPE(typevartuple);
MAKE_TYPE(paramspec);
MAKE_TYPE(paramspecargs);
MAKE_TYPE(paramspeckwargs);
MAKE_TYPE(constevaluator);
return 0;
}MAKE_TYPE calls PyType_FromSpec and stores the result in
interp->cached_objects. These types are then imported into typing from
_typing:
from _typing import (
TypeVar, ParamSpec, TypeVarTuple,
ParamSpecArgs, ParamSpecKwargs, TypeAliasType, ...
)Substitution protocol
Each type implements __typing_subst__ and __typing_prepare_subst__,
which the typing machinery calls when applying a generic alias to concrete
arguments. For TypeVar, __typing_prepare_subst__ looks up the TypeVar’s
index among the alias’s __parameters__ and, if a default exists and no
argument was supplied, appends the default value to the args tuple. Bare
TypeVarTuple substitution raises TypeError (“Substitution of bare
TypeVarTuple is not supported”).
TypeAliasType
typealiasobject is a separate struct with name, qualname, type_params,
compute_value, value, and module. It supports lazy evaluation of its
value (via compute_value), mirroring the constevaluator pattern, and is
created by _Py_make_typealias.
Key takeaways
TypeVar,TypeVarTuple, andParamSpecare GC-tracked C structs inObjects/typevarobject.c, cached per-interpreter incached_objects, created in_Py_initialize_generic.TypeVaruniquely supportsconstraints; all three sharebound, variance flags, and optional defaults.- Defaults/bounds can be provided eagerly or as
evaluate_*callables, with results cached on first access; eager values are wrapped in_ConstEvaluatorwhen exposed. NoDefaultis an immortal singleton used as the “no default” marker.ParamSpec.args/.kwargsreturn on-demandParamSpecArgs/ParamSpecKwargsobjects pointing back at the origin.TypeVarandParamSpecsupport|viamake_union, producingtyping.Union.- The types are exposed to Python through the
_typingextension module and re-exported byLib/typing.py.
Further reading
Objects/typevarobject.c– all structs, allocators, dealloc/traverse/clear, getters,__new__,__typing_subst__,_Py_initialize_generic.Include/internal/pycore_typevarobject.h–_Py_make_typevar,_Py_make_paramspec,_Py_make_typevartuple,_Py_make_typealias,_Py_set_typeparam_default.Lib/typing.py– re-exports from_typingand the Python-level typing machinery.Objects/clinic/typevarobject.c.h– Argument Clinic generated code.