Objects/genericaliasobject.c: GenericAlias, __class_getitem__, and Typing Integration
Objects/genericaliasobject.c implements types.GenericAlias, the runtime
object produced by expressions like list[int] or dict[str, int]. An alias
wraps an origin class and a parameter tuple without creating a new class. Most
attribute access is proxied back to the origin, so list[int].append still
resolves. The file also provides the __class_getitem__ descriptor that built-in
types use to enter the alias machinery, and the __mro_entries__ hook that lets
class Foo(list[int]) resolve list as the real base.
Map
| Lines | Symbol | Role |
|---|---|---|
| 1-44 | gaobject struct | origin, args, parameters, starred fields; parameters is lazily built |
| 45-90 | ga_traverse | GC visitor; visits origin, args, parameters |
| 91-200 | ga_repr, ga_repr_items_list | Formats list[int] and *tuple[int]; handles ParamSpec list args |
| 201-403 | _Py_make_parameters, _Py_subs_parameters | Collect TypeVar/ParamSpec/TypeVarTuple from args; substitute them |
| 404-605 | ga_getitem (__getitem__) | Subscript an alias to substitute parameters; returns a new alias |
| 606-700 | ga_hash, ga_call, ga_richcompare | Hash = hash(origin) ^ hash(args); call forwards to origin; eq compares starred + origin + args |
| 653-695 | attr_exceptions, attr_blocked, ga_getattro | Own-attribute set, blocked-attribute set, and the three-way proxy dispatch |
| 742-820 | ga_mro_entries, ga_instancecheck, ga_subclasscheck | PEP 560 and PEP 585 hooks; isinstance(x, list[int]) always raises TypeError |
| 821-895 | ga_methods, ga_members, ga_properties | Descriptor tables wired into Py_GenericAliasType |
| 896-960 | ga_new, setup_ga | tp_new for types.GenericAlias(origin, args); setup_ga normalizes args to a tuple |
| 961-1014 | ga_iter, gaiterobject | One-shot iterator that yields a starred=True copy for *tuple[int] unpacking |
| 1014-1060 | Py_GenericAlias, Py_GenericAliasType | Public entry point used by built-in tp_class_getitem slots |
Reading
gaobject struct and the starred flag
The starred bit marks aliases created by the * unpack syntax in a
subscription context (*tuple[int, ...]). A starred alias carries a
__unpacked__ attribute of True and exposes
__typing_unpacked_tuple_args__ when the origin is tuple. The bit threads
through ga_iter so iterating a starred alias yields the alias itself
(enabling [*tuple[int]] in TypeAlias expressions).
// CPython: Objects/genericaliasobject.c:15 gaobject
typedef struct {
PyObject_HEAD
PyObject *origin; /* the parameterized class, e.g. list */
PyObject *args; /* parameter tuple, e.g. (int,) */
PyObject *parameters; /* TypeVar tuple, lazily built from args */
bool starred; /* True when created by *alias for unpacking */
} gaobject;
setup_ga is called from both Py_GenericAlias (the C entry point for
list[int]) and ga_new (the Python-level types.GenericAlias(list, int)
constructor). It normalizes a non-tuple args value into a one-item tuple so
all downstream code can assume args is always a tuple.
ga_getattro: own / blocked / proxy dispatch
Attribute lookup on an alias follows a three-way rule. Attributes in
attr_exceptions are handled by the alias's own type (via GenericGetAttr).
Attributes in attr_blocked are also handled by the alias itself so they
don't leak through to the origin class, which would return class-level
answers that should not apply to the alias. Everything else proxies to the
origin.
// CPython: Objects/genericaliasobject.c:674 ga_getattro
static PyObject *
ga_getattro(PyObject *self, PyObject *name)
{
gaobject *alias = (gaobject *)self;
if (PyUnicode_Check(name) &&
is_in_set(name, attr_blocked)) /* __bases__, __copy__, etc. */
return PyObject_GenericGetAttr(self, name);
if (PyUnicode_Check(name) &&
is_in_set(name, attr_exceptions)) /* __origin__, __args__, ... */
return PyObject_GenericGetAttr(self, name);
return PyObject_GetAttr(alias->origin, name); /* proxy */
}
The attr_blocked set matters for pickling and copy: if __copy__ were
proxied to list, copy.copy(list[int]) would shallow-copy the origin class
rather than the alias.
ga_subscript: nested parameterization
Subscribing an already-parameterized alias (list[T][int]) triggers
ga_getitem, which collects the TypeVar parameters from args, then calls
_Py_subs_parameters to substitute the supplied item values for each
TypeVar in order. The result is a fresh GenericAlias with the same origin
but substituted args.
// CPython: Objects/genericaliasobject.c:572 ga_getitem
static PyObject *
ga_getitem(PyObject *self, PyObject *item)
{
gaobject *alias = (gaobject *)self;
/* Lazily build the parameters tuple from TypeVars in args. */
if (alias->parameters == NULL) {
alias->parameters = _Py_make_parameters(alias->args);
if (alias->parameters == NULL)
return NULL;
}
PyObject *new_args = _Py_subs_parameters(self, alias->args,
alias->parameters, item);
if (new_args == NULL)
return NULL;
PyObject *res = Py_GenericAlias(alias->origin, new_args);
((gaobject *)res)->starred = alias->starred;
Py_DECREF(new_args);
return res;
}
When parameters is empty the alias has no free TypeVars left; CPython raises
TypeError: list[int] is not a generic class. gopy reproduces this in
subsParameters.
ga_instancecheck and mro_entries
isinstance(x, list[int]) is deliberately a TypeError because runtime
type-checking against a parameterized generic is not safe (the type parameters
are erased). The error message was improved in 3.12 to hint at isinstance(x, list).
__mro_entries__ is the PEP 560 hook used when an alias appears as a base
class. class Foo(list[int]) calls list[int].__mro_entries__((list[int],))
which returns (list,). The MRO machinery then replaces the alias with list
in the bases tuple before creating the new type.
// CPython: Objects/genericaliasobject.c:742 ga_mro_entries
static PyObject *
ga_mro_entries(PyObject *self, PyObject *args)
{
gaobject *alias = (gaobject *)self;
return PyTuple_Pack(1, alias->origin);
}
// CPython: Objects/genericaliasobject.c:756 ga_instancecheck
static PyObject *
ga_instancecheck(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(arg))
{
PyErr_SetString(PyExc_TypeError,
"isinstance() argument 2 cannot be a parameterized generic");
return NULL;
}
typing_unpacked_tuple_args in 3.14
3.14 formalized __typing_unpacked_tuple_args__ as a stable attribute on
GenericAlias. It returns args when starred=True and origin is tuple,
otherwise None. This allows typing.get_args(*tuple[int, str]) to work
without reaching into private internals.
// CPython: Objects/genericaliasobject.c:836 ga_typing_unpacked_tuple_args (3.14)
static PyObject *
ga_typing_unpacked_tuple_args(gaobject *alias, void *Py_UNUSED(ignored))
{
if (alias->starred && alias->origin == (PyObject *)&PyTuple_Type) {
return Py_NewRef(alias->args);
}
Py_RETURN_NONE;
}
In gopy this is the __typing_unpacked_tuple_args__ getset registered in the
second init() block of objects/generic_alias.go.
gopy notes
GenericAlias in objects/generic_alias.go mirrors gaobject field-for-field.
The starred bool maps directly. parameters starts nil and is populated
lazily by makeParameters(ga.args), matching CPython's lazy allocation.
gaGetattro implements the three-way dispatch with two Go maps
(gaAttrOwn and gaAttrBlocked), matching attr_exceptions and
attr_blocked from CPython.
ga_iter collapses to gaIter, which wraps a single-element list containing
a starred copy of the alias. A dedicated gaiterobject is not ported; the
existing seqiter path is sufficient for the one-shot case.
__mro_entries__ is registered as a MethodDescr and returns
NewTuple([]Object{ga.origin}), matching ga_mro_entries exactly.
_Py_make_parameters and _Py_subs_parameters are stubbed in
makeParameters and subsParameters pending a full TypeVar port. The stubs
raise the same TypeError messages as CPython so test code that catches
those errors does not need adjustment when the real implementation lands.