CPython's type Implementation in C (the Metaclass)
The PyTypeObject struct, method slots, tp_dict, MRO computation, heap vs static types, and type creation traced through the CPython 3.15 source.
type is the metaclass of every Python object: calling type(obj) returns the
class of obj, and class Foo: ultimately calls type.__new__. In CPython
the same object that represents type in Python is PyType_Type, and every
class is a PyTypeObject (or, for classes created at runtime, a
PyHeapTypeObject). This article traces how that machinery is implemented in
Objects/typeobject.c and Include/cpython/object.h in CPython 3.15.
The object struct
Every Python object begins with PyObject_VAR_HEAD. A type object adds dozens
of slots that describe how instances behave, plus the things that make a type a
first-class object: its tp_dict, bases, and MRO. The 3.15 layout is:
// Include/cpython/object.h
struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name;
Py_ssize_t tp_basicsize, tp_itemsize;
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async;
reprfunc tp_repr;
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
PyBufferProcs *tp_as_buffer;
unsigned long tp_flags;
const char *tp_doc;
traverseproc tp_traverse;
inquiry tp_clear;
richcmpfunc tp_richcompare;
Py_ssize_t tp_weaklistoffset;
getiterfunc tp_iter;
iternextfunc tp_iternext;
PyMethodDef *tp_methods;
PyMemberDef *tp_members;
PyGetSetDef *tp_getset;
PyTypeObject *tp_base; /* borrowed on static, strong on heap */
PyObject *tp_dict; /* the class __dict__ */
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free;
inquiry tp_is_gc;
PyObject *tp_bases; /* tuple of direct bases */
PyObject *tp_mro; /* method resolution order tuple */
PyObject *tp_cache; /* unused */
void *tp_subclasses; /* dict, or index for static builtins */
PyObject *tp_weaklist;
destructor tp_del;
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
unsigned char tp_watched;
uint16_t tp_versions_used;
_Py_iteritemfunc _tp_iteritem;
};The slot fields (tp_methods, tp_members, tp_getset) are arrays that
PyType_Ready turns into real descriptors in tp_dict. tp_as_number and
friends are method suites: pointers to structs of function pointers that
implement +, len(), [], etc. They are populated either statically (for
built-in types) or by add_operators() during readying.
The real layout: PyHeapTypeObject
A type created at runtime (class Foo:) is not just a PyTypeObject. It is a
PyHeapTypeObject, whose first member is the PyTypeObject (so it can be
cast back and forth), followed by the method-suite structs and heap-only
metadata:
// Include/cpython/object.h
typedef struct _heaptypeobject {
PyTypeObject ht_type;
PyAsyncMethods as_async;
PyNumberMethods as_number;
PyMappingMethods as_mapping;
PySequenceMethods as_sequence;
PyBufferProcs as_buffer;
PyObject *ht_name, *ht_slots, *ht_qualname;
struct _dictkeysobject *ht_cached_keys;
PyObject *ht_module;
char *_ht_tpname;
void *ht_token;
struct _specialization_cache _spec_cache;
#ifdef Py_GIL_DISABLED
Py_ssize_t unique_id;
#endif
} PyHeapTypeObject;The distinction is exposed by the Py_TPFLAGS_HEAPTYPE flag. Static types
(int, list, str, …) are immortal, singletons stored in the binary;
heap types are allocated with PyObject_GC_New and reference-counted.
tp_dict: the class dictionary
tp_dict is the __dict__ of the class. For heap types it is a real
dict created during type creation. For static built-in types CPython stores
tp_dict per-interpreter in managed_static_type_state (so each interpreter
gets its own dict), which is why lookup_tp_dict branches on
_Py_TPFLAGS_STATIC_BUILTIN:
// Objects/typeobject.c
static inline PyObject *lookup_tp_dict(PyTypeObject *self) {
if (self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
PyInterpreterState *interp = _PyInterpreterState_GET();
managed_static_type_state *state = _PyStaticType_GetState(interp, self);
return state->tp_dict;
}
return self->tp_dict;
}Attribute lookup (type.__getattribute__, i.e. _PyType_Lookup) searches
tp_dict of the type and then walks tp_mro, reading each base’s tp_dict.
When a class is modified, PyType_Modified invalidates the version tag and
notifies watchers.
MRO
The MRO is stored as a tuple in tp_mro, with the type itself first and
object last. It is computed by mro_internal, which calls the type’s (or
metaclass’s) mro() and falls back to C3 linearization:
// Objects/typeobject.c
static int mro_internal(PyTypeObject *type, int initial, PyObject **p_old_mro) {
...
old_mro = Py_XNewRef(lookup_tp_mro(type));
new_mro = mro_invoke(type); /* may re-enter */
reent = (lookup_tp_mro(type) != old_mro);
...
set_tp_mro(type, new_mro, initial);
type_mro_modified(type, new_mro);
...
}mro_internal also records the old MRO so that, when bases change, subclasses
can be updated. type_mro_modified checks whether the type is eligible for the
attribute cache (version tag): a custom mro() or a non-subtype base clears
tp_versions_used to _Py_ATTR_CACHE_UNUSED, disabling caching for that
type and its subclasses.
Type creation
Creating a class (class Foo(Bar): ...) calls type.__new__, which in C is
type_new -> ultimately type_from_slots_or_spec for the C API path. The
public entry points are:
// Objects/typeobject.c
PyObject *PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
PyType_Spec *spec, PyObject *bases);
PyObject *PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases);
PyObject *PyType_FromSpec(PyType_Spec *spec);A PyType_Spec describes name, basicsize, itemsize, flags and a list of
PyType_Slot entries mapping slot IDs (Py_tp_init, Py_tp_methods, …)
to function pointers. type_from_slots_or_spec allocates a
PyHeapTypeObject, copies the spec into the slots, sets tp_base/tp_bases,
creates tp_dict, and finally calls PyType_Ready.
PyType_Ready is the choke point that finishes initialization:
// Objects/typeobject.c
int PyType_Ready(PyTypeObject *type) {
if (type->tp_flags & Py_TPFLAGS_READY) return 0;
assert(!(type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN));
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
type_add_flags(type, Py_TPFLAGS_IMMUTABLETYPE);
_Py_SetImmortalUntracked((PyObject *)type);
}
int res;
BEGIN_TYPE_LOCK();
if (!(type->tp_flags & Py_TPFLAGS_READY)) {
res = type_ready(type, 1, 1);
}
END_TYPE_LOCK();
return res;
}type_ready (called under the type lock) fills in inherited slots from the
base, calls add_operators to turn tp_methods/tp_members/tp_getset
into descriptors in tp_dict, computes tp_mro via mro_internal, sets
tp_flags |= Py_TPFLAGS_READY, and assigns a version tag. After this the type
is usable.
Heap vs static types
Static types (PyLong_Type, PyList_Type, …) are compile-time
PyTypeObject constants. They carry _Py_TPFLAGS_STATIC_BUILTIN (for the
core builtins) or are plain static types in extension modules. They are
immortal and, for builtins, have per-interpreter tp_dict/tp_subclasses/
tp_weaklist stored in managed_static_type_state. init_static_type
registers this state and calls type_ready:
// Objects/typeobject.c
static int init_static_type(PyInterpreterState *interp, PyTypeObject *self,
int isbuiltin, int initial) {
...
type_add_flags(self, _Py_TPFLAGS_STATIC_BUILTIN);
type_add_flags(self, Py_TPFLAGS_IMMUTABLETYPE);
managed_static_type_state_init(interp, self, isbuiltin, initial);
...
res = type_ready(self, initial, 1);
...
}Heap types differ in several ways:
- Allocated on the heap and reference-counted (
Py_TPFLAGS_HAVE_GCusually). Py_TPFLAGS_HEAPTYPEis set;tp_name/ht_namelive in thePyHeapTypeObject.- Mutable by default: attributes can be added,
__bases__can change, MRO recomputed. tp_baseis a strong reference; for static types it is a borrowed reference.- Version tags come from a per-interpreter counter (
NEXT_VERSION_TAG) rather than the global counter used for static types.
The Py_TPFLAGS_IMMUTABLETYPE flag (set by PyType_Ready for any non-heap
type, and available to heap types via Py_TPFLAGS_IMMUTABLETYPE) controls
whether __setattr__/__delattr__ on the type itself are allowed, which is
what makes a class “frozen”.
Type lookup and the version tag
_PyType_Lookup walks the MRO and reads tp_dict. To make attribute access
fast, CPython caches results keyed by tp_version_tag:
// Objects/typeobject.c (conceptually)
PyObject *_PyType_Lookup(PyTypeObject *type, PyObject *name) {
...
/* version tag fast path */
}assign_version_tag (called lazily) assigns a tag to a type only after all
its bases have tags, so a cached lookup is valid for the whole hierarchy.
PyType_Modified clears the tag (setting it to 0) and recursively clears
subclasses, forcing re-lookup. This is why mutating a base class invalidates
caches everywhere below it.
type.__call__ and instance creation
When you write Foo(x), Python calls type.__call__(Foo, x). In C that is
type_call, which runs tp_new to allocate and initialize the instance, then
tp_init to initialize it, and finally returns the object (or propagates an
error). The slots are inherited from the base during type_ready, so a type
that does not define tp_new gets object.__new__ (i.e. slot_tp_new):
// Objects/typeobject.c
static PyObject *type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) {
PyObject *obj;
...
obj = type->tp_new(type, args, kwds);
if (obj == NULL) return NULL;
if (type->tp_init != NULL && type->tp_init(obj, args, kwds) < 0) {
Py_DECREF(obj);
return NULL;
}
return obj;
}tp_alloc (default PyType_GenericAlloc) allocates the instance using
tp_basicsize/tp_itemsize and sets ob_type. tp_free is the matching
deallocator. For GC-tracked types tp_alloc calls PyObject_GC_New and
tp_free calls PyObject_GC_Del.
Slots and inheritance
Slots are not inherited field-by-field naively. type_ready calls
inherit_slots (and add_operators), which copies function pointers from
the base when the derived type leaves a slot NULL, and merges the method suites
(tp_as_number, etc.) by copying individual function pointers. This is why a
subclass of int automatically gets nb_add unless it overrides __add__.
The tp_methods/tp_members/tp_getset arrays are processed by
add_methods/add_members/add_getset, which build PyCFunction/member/
getset descriptors and insert them into tp_dict. After that the arrays are no
longer needed for lookup (the dict holds the descriptors).
Subclassing rules
PyType_Ready and type_new enforce that a base type must have
Py_TPFLAGS_BASETYPE set; otherwise type(...) raises “is not an acceptable
base type”. object, type and most builtins set this flag. Some types (e.g.
list historically, though now subclassable) clear it to forbid subclassing in
C.
type_is_subtype and is_subtype_with_mro are used to validate MROs. A type
cannot be its own base, and the MRO must be consistent (C3 merge). If no
linearization exists, type() raises TypeError: Cannot create a consistent method resolution order.
type_new in detail
type_new (the C implementation of type.__new__) does the heavy lifting for
class statements:
- Validate
name,bases,dict. - Choose the metaclass: if
basesis non-empty, use the most derived metaclass among the bases’ metaclasses; otherwise use the provided metaclass ortype. - Allocate a
PyHeapTypeObjectsized fortp_basicsize + nslots * sizeof(void*)when__slots__is present. - Copy
tp_name(stored in_ht_tpname),ht_name,ht_qualname. - Set
tp_baseto the best base (orobject),tp_basesto the tuple. - Call
PyType_Ready.
For __slots__, type_new creates a member_descriptor for each slot and
stores the slot index in the instance dict layout (tp_dictoffset,
tp_weaklistoffset).
Managed dicts and __dict__
Heap types may have a managed __dict__ (Py_TPFLAGS_MANAGED_DICT) instead
of a fixed tp_dictoffset. In 3.15 managed dicts are the default for heap
types, storing the instance dict in a side table indexed by the object. This
saves memory when most instances have no extra attributes. tp_dictoffset
is set to 0 in that case and PyObject_GenericGetAttr uses
_PyObject_GetDictPtr.
Garbage collection
Heap types set Py_TPFLAGS_HAVE_GC and provide tp_traverse/tp_clear
(generated by the GC machinery for the type’s slots and tp_dict). Static types
that contain references also set these flags. tp_is_gc answers whether a
particular instance is tracked. _PyObject_GC_TRACK is called in tp_alloc
for GC types.
Free-threading notes
In Py_GIL_DISABLED builds, several fields become per-interpreter or
atomically accessed: tp_dict, tp_mro, tp_bases for static builtins live
in managed_static_type_state; tp_version_tag uses atomic loads/stores;
tp_subclasses is per-interpreter. Type mutation takes the type lock
(TYPE_LOCK) and may stop the world. New types are marked “revealed” via
_Py_TYPE_REVEALED_FLAG only after they are fully initialized, so other
threads never observe a half-ready type.
type itself
PyType_Type is defined statically in Objects/typeobject.c with
tp_name = "type", tp_basicsize = sizeof(PyHeapTypeObject),
tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE,
and slots pointing at type_* functions (type_repr, type_call,
type_getattro, etc.). Its metaclass is itself (PyType_Type.tp_type = &PyType_Type),
which is the fixed point that makes type(type) is type.
Key takeaways
typeisPyType_Type; every class is aPyTypeObject, and runtime classes arePyHeapTypeObjects (heap-allocated, GC-tracked).- Slots (
tp_*) describe instance behavior; method suites (tp_as_number, …) group operator implementations. tp_dictis the class__dict__; static builtins store theirs per-interpreter.- The MRO is a tuple in
tp_mro, computed bymro_internal(C3 by default, custommro()allowed). PyType_Readyfinishes a type: inheritance, operators, MRO, flags, version tag.- Static types are immortal and immutable by default; heap types are mutable and reference-counted.
- Version tags power the attribute-lookup cache;
PyType_Modifiedinvalidates them.
Type watchers and the attribute cache
CPython 3.11+ added PyType_AddWatcher/PyType_Watch so that tools
(jit/optimizer, debuggers) can be notified when a type changes. A watcher bit
is stored in tp_watched; when PyType_Modified runs it invokes every
registered callback for that type. PyType_Modified also clears the version
tag and walks subclasses, so watchers see a consistent invalidation.
tp_cache is a legacy field (always NULL now); caching moved to the
per-interpreter type_cache hash table keyed by (version, name).
Further reading
Objects/typeobject.c– all of the above.Include/cpython/object.h–PyTypeObjectandPyHeapTypeObject.Include/cpython/object.h(type slots) andInclude/pytypedefs.h.Objects/typeobject.c(mro_internal,type_ready,type_from_slots_or_spec).