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:

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

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

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

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

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

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

c
// 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_GC usually).
  • Py_TPFLAGS_HEAPTYPE is set; tp_name/ht_name live in the PyHeapTypeObject.
  • Mutable by default: attributes can be added, __bases__ can change, MRO recomputed.
  • tp_base is 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:

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

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

  1. Validate name, bases, dict.
  2. Choose the metaclass: if bases is non-empty, use the most derived metaclass among the bases’ metaclasses; otherwise use the provided metaclass or type.
  3. Allocate a PyHeapTypeObject sized for tp_basicsize + nslots * sizeof(void*) when __slots__ is present.
  4. Copy tp_name (stored in _ht_tpname), ht_name, ht_qualname.
  5. Set tp_base to the best base (or object), tp_bases to the tuple.
  6. 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

  • type is PyType_Type; every class is a PyTypeObject, and runtime classes are PyHeapTypeObjects (heap-allocated, GC-tracked).
  • Slots (tp_*) describe instance behavior; method suites (tp_as_number, …) group operator implementations.
  • tp_dict is the class __dict__; static builtins store theirs per-interpreter.
  • The MRO is a tuple in tp_mro, computed by mro_internal (C3 by default, custom mro() allowed).
  • PyType_Ready finishes 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_Modified invalidates 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 – PyTypeObject and PyHeapTypeObject.
  • Include/cpython/object.h (type slots) and Include/pytypedefs.h.
  • Objects/typeobject.c (mro_internal, type_ready, type_from_slots_or_spec).

PythonCpythonInternalsTypeMetaclass