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.TypeVar
  • typevartupleobject -> typing.TypeVarTuple
  • paramspecobject -> typing.ParamSpec
  • paramspecattrobject -> typing.ParamSpecArgs / ParamSpecKwargs
  • typealiasobject -> typing.TypeAliasType
  • _ConstEvaluator and NoDefaultType (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

c
// 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:

  • TypeVar is the only one that supports constraints (a tuple of types) in addition to bound; TypeVarTuple and ParamSpec only have bound.
  • All three share variance flags (covariant, contravariant, infer_variance) and an optional default_value.
  • ParamSpecArgs/ParamSpecKwargs are 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:

c
// 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):

c
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”:

c
// 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

c
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:

  • covariant and contravariant cannot both be true.
  • infer_variance cannot be combined with explicit variance.
  • bound is passed through type_check (which calls typing._type_check).
  • A single constraint is forbidden; zero constraints becomes NULL; constraints and bound are 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

c
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:

c
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:

c
typedef struct {
    PyObject_HEAD
    PyObject *__origin__;
} paramspecattrobject;

ParamSpec.args and ParamSpec.kwargs (getset descriptors) construct them on demand:

c
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:

c
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

c
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:

python
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, and ParamSpec are GC-tracked C structs in Objects/typevarobject.c, cached per-interpreter in cached_objects, created in _Py_initialize_generic.
  • TypeVar uniquely supports constraints; all three share bound, 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 _ConstEvaluator when exposed.
  • NoDefault is an immortal singleton used as the “no default” marker.
  • ParamSpec.args/.kwargs return on-demand ParamSpecArgs/ParamSpecKwargs objects pointing back at the origin.
  • TypeVar and ParamSpec support | via make_union, producing typing.Union.
  • The types are exposed to Python through the _typing extension module and re-exported by Lib/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 _typing and the Python-level typing machinery.
  • Objects/clinic/typevarobject.c.h – Argument Clinic generated code.

PythonCpythonInternalsTypingTypevar