Types
A type in CPython is a PyTypeObject (which is itself a
PyObject). The runtime treats user-defined classes and built-in
types uniformly: both go through PyType_Type (the metaclass);
both fill the same slot vector; both participate in the same MRO
algorithm. The difference is administrative. built-in types are
statically allocated, user types are heap allocated. but the
dispatch path is identical.
Where the code lives
| File | Role |
|---|---|
Objects/typeobject.c | Type creation, slot wiring, descriptor lookup, MRO. |
Include/cpython/object.h | PyTypeObject, slot signatures. |
Include/internal/pycore_typeobject.h | Internal helpers; the slotdefs table. |
Objects/descrobject.c | Method, member, and getset descriptors. |
PyType_Type
PyType_Type is type's C representation. It is statically
declared in Objects/typeobject.c:
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type",
sizeof(PyHeapTypeObject) - sizeof(PyMemberDef), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
/* tp_dealloc, tp_repr, ... */
type_call, /* tp_call */
/* ... */
type_new, /* tp_new */
};
PyType_Type.tp_call is what runs when you write MyClass(x):
it calls MyClass->tp_new(MyClass, args) and then
MyClass->tp_init(instance, args). The metaclass mechanism is
the same in reverse: writing class Foo(metaclass=Meta) makes
Meta.tp_call build the new class, which goes through
Meta.tp_new (typically type_new).
Built-in versus heap types
- Static types. Defined at C compile time. Their
PyTypeObjectis in.data/.rodata; the type itself does not get freed. Examples:int,str, every built-in collection. - Heap types (
PyHeapTypeObject). Created at runtime bytype()or byclass. The struct has extra trailing fields for the slot tables, the name, the qualname, the documentation, and the descriptor cache. Heap types are reference-counted like any other object.
The eval loop does not care which kind a type is; the metaclass hides the distinction.
Slot dispatch
Slot fields on PyTypeObject are function pointers. The runtime
calls them by indirect call:
PyObject *result = type->tp_call(callable, args, kwargs);
There is no virtual table walk; the slot is at a known offset on the type. The cost is a load and an indirect call.
Slot inheritance: when a subclass leaves a slot NULL, type creation walks the MRO at class-build time and copies the first non-NULL ancestor's slot in. The copy means the dispatch always finds the slot on the immediate type. no MRO walk at call time.
The slotdefs table
Python dunders such as __hash__ live in tp_dict; the C slot
tp_hash is the implementation. The slotdefs table in
Objects/typeobject.c is what bridges them:
static slotdef slotdefs[] = {
SLOT_OFFSET("__getitem__", tp_as_mapping, mp_subscript, slot_mp_subscript, ...),
SLOT_OFFSET("__hash__", tp_hash, slot_tp_hash, ...),
/* ~150 entries */
};
type.__init__ walks slotdefs and, for each entry whose dunder
is defined on the class, installs a slot_* adapter into the
slot. The adapter looks up the dunder via the descriptor protocol
and calls it. Conversely, when a user reads MyClass.__hash__,
attribute lookup finds the slot's slot_tp_hash adapter and
returns a method object that calls it.
The two-way wiring is what makes the rule "always go through slots" hold: writing a class in Python or in C lands on the same fast path.
MRO
Method resolution order is computed once at type-creation time by the C3 linearisation algorithm:
/* Objects/typeobject.c mro_implementation */
static PyObject *mro_implementation(PyTypeObject *type);
The result is stored in tp_mro as a tuple. Slot inheritance,
attribute lookup, and super() all read from it.
Diamond inheritance is resolved deterministically: C3 picks a
linearisation that respects each base's order and the local
declaration order, raising TypeError if no consistent order
exists.
Attribute lookup
getattr(obj, name) calls tp_getattro, which defaults to
PyObject_GenericGetAttr:
/* Objects/object.c (sketch) PyObject_GenericGetAttr */
descr = _PyType_Lookup(Py_TYPE(obj), name); /* walk MRO */
if (descr && Py_TYPE(descr)->tp_descr_get) {
/* data descriptor wins over instance dict */
if (Py_TYPE(descr)->tp_descr_set != NULL) {
return Py_TYPE(descr)->tp_descr_get(descr, obj, type);
}
}
if (instance has dict and name in it) {
return dict[name];
}
if (descr) {
if (Py_TYPE(descr)->tp_descr_get) return ...->tp_descr_get(...);
return descr;
}
raise AttributeError;
The rule: data descriptors (those with tp_descr_set) win over
instance dicts; non-data descriptors lose to them; everything
loses to a data descriptor. Properties, slot wrappers, and member
descriptors are data descriptors; methods are non-data
descriptors.
The specializer (see specializer) caches the result of this lookup per call site so subsequent reads at the same site at the same type skip the MRO walk.
Descriptors
Three flavours:
- Method descriptor. A function found in a class. Reading
instance.methodrunstp_descr_get, which constructs a bound method. - Slot descriptor. A C-level field exposed through a member
descriptor (
PyMemberDef). Reads and writes touch thePy_T_*field at a known offset. - Getset descriptor. A C-level property; reads run a getter, writes run a setter.
User-defined descriptors are anything implementing
__get__/__set__/__delete__; property, staticmethod,
classmethod, and functools.cached_property are the visible
examples.
slots
A class with __slots__ does not allocate an instance dict.
Instead the slot names become member descriptors and the
instance allocation reserves one field per slot at fixed offsets.
This saves memory (no per-instance dict) and time (no hash
lookup for attribute access) at the cost of inflexibility (no
new attributes).
The implementation lives in type_new_impl:
__slots__ are turned into PyMemberDef entries on the type,
and tp_basicsize is grown to accommodate the slot storage.
Metaclasses
class Foo(metaclass=Meta): ... calls Meta(name, bases, ns).
Meta may inherit from type and override __init__ /
__new__ to customise class construction. ABCs (PEP 3119) and
enum classes (PEP 435) are the canonical users.
Type instantiation
x = list((1, 2, 3))
LOAD_GLOBAL listreturnsPyList_Type.CALLdispatches toPyList_Type.tp_call, which istype_call.type_callrunslist.__new__(list, (1, 2, 3)), thenlist.__init__(x, (1, 2, 3)).- The instance is returned.
The specializer rewrites step 2 to CALL_BUILTIN_O for many
common types, skipping the generic dispatch.
Generic types (PEP 695)
PEP 695 added class Container[T]: syntax. The compiler
introduces a TypeParametersBlock that holds the parameter as a
local; the class body runs as a child scope where the parameter
is visible. At runtime, the parameter object (TypeVar,
TypeVarTuple, ParamSpec) lives in type.__type_params__.
The class itself becomes generic via the
__class_getitem__ protocol; Container[int] returns a
types.GenericAlias.
CPython 3.14 changes
- PEP 695 fully wired through
tp_dict,__type_params__, and the ASTtype_paramnodes. - Specialised
tp_callpaths. Many built-in types gained fast-path call dispatch; the specializer reaches them viaCALL_TYPE_1,CALL_BUILTIN_O, and similar. __static_attributes__. A new tuple summarising the attribute names a class assigns in its body. used by the inspector module and for documentation tooling.
PEP touchpoints
- PEP 3115. Metaclass syntax.
- PEP 487.
__init_subclass__and__set_name__. - PEP 526, 591, 695. Type annotations and parameters.
- PEP 615, 689. Static immortal types.
Reference
Objects/typeobject.c,Objects/descrobject.c,Include/cpython/object.h,Include/internal/pycore_typeobject.h.- PEP 487. Simpler customisation of class creation.
- PEP 695. Type parameter syntax.