Type Objects and PyTypeObject
In CPython every value is a
PyObject, and everyPyObjectcarries a pointer to a type object that decides how it behaves. A type object is itself an object — specifically an instance of the typetype— represented in C by the large structPyTypeObject(struct _typeobjectinInclude/cpython/object.h, as of CPython 3.14.0). That struct is not a class definition in the abstract sense: it is a flat table of C function pointers and metadata, thetp_*“slots,” that the interpreter consults every time it does anything with an instance —repr(x),x(),hash(x),x.attr,x + y,len(x). Understanding the layout ofPyTypeObjectis understanding how Python dispatch actually works below the source line. This note walks the struct field by field; its sibling Type Slots and the Protocol Tables covers the sub-structs (tp_as_number,tp_as_sequence, …) and the machinery that keeps a C slot and a Python dunder method in sync.
Mental Model — A Type Is an Object Whose Type Is type
The single most important fact: a type object is an ordinary heap object that happens to describe other objects. Its own header (PyObject_VAR_HEAD) points at the type type, so type(int) is type, type(str) is type, even type(type) is type — the chain is closed, type is its own type. This is verifiable directly:
>>> type(int) is type # int's type is `type`
True
>>> type(type) is type # `type`'s type is itself — the chain bottoms out
TrueSo there are two distinct “is-a” relationships layered on the same struct. The instance-of relationship runs through the object header’s ob_type pointer (an object → its type). The inheritance relationship runs through tp_base/tp_mro (a type → its base types). int is an instance of type and inherits from object; those are different axes. The metaclass story — why type is its own type, how a custom metaclass slots in — belongs to The type and object Relationship and Metaclasses; here we focus on the struct itself.
graph TD subgraph "instance-of (ob_type)" i["x = 42<br/>(a PyLongObject)"] -->|ob_type| T_int["int<br/>(a PyTypeObject)"] T_int -->|ob_type| T_type["type<br/>(a PyTypeObject)"] T_type -->|ob_type| T_type end subgraph "inherits-from (tp_base / tp_mro)" T_int -.->|tp_base| T_obj["object"] T_type -.->|tp_base| T_obj end
Diagram: the two orthogonal arrows on a type object. Solid arrows are the ob_type (instance-of) pointer in every object’s header; they climb to type, which is its own type. Dashed arrows are tp_base (inheritance); both int and type ultimately inherit from object. The insight: a PyTypeObject participates in both relationships at once — it is described by type and it describes its own instances.
The Struct, Field by Field
Below is the actual PyTypeObject definition from Include/cpython/object.h (CPython 3.14.0), reproduced verbatim, then walked in groups. The fields appear in a fixed memory order that matters: heap types allocate the protocol sub-structs immediately after the type (see Type Slots and the Protocol Tables), and the slot-update machinery relies on offsetof into this exact layout.
struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
/* Methods to implement standard operations */
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async;
reprfunc tp_repr;
/* Method suites for standard classes */
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
/* More standard operations (here for binary compatibility) */
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;
/* Flags to define presence of optional/expanded features */
unsigned long tp_flags;
const char *tp_doc; /* Documentation string */
/* call function for all accessible objects */
traverseproc tp_traverse;
/* delete references to contained objects */
inquiry tp_clear;
/* rich comparisons */
richcmpfunc tp_richcompare;
/* weak reference enabler */
Py_ssize_t tp_weaklistoffset;
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;
/* Attribute descriptor and subclassing stuff */
PyMethodDef *tp_methods;
PyMemberDef *tp_members;
PyGetSetDef *tp_getset;
PyTypeObject *tp_base;
PyObject *tp_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; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
void *tp_subclasses; /* for static builtin types this is an index */
PyObject *tp_weaklist;
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6 */
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
/* bitset of which type-watchers care about this type */
unsigned char tp_watched;
uint16_t tp_versions_used;
};Identity and Size — tp_name, tp_basicsize, tp_itemsize
PyObject_VAR_HEAD is the variable-length object header (reference count, type pointer, and an ob_size count of variable items — see PyObject and the Object Header). A type uses the var header because a few types (notably type itself) carry trailing data.
tp_name is the C string used in repr and error messages, conventionally "module.Qualname" (e.g. "collections.OrderedDict"). For heap types the displayed name comes from __qualname__/__module__; for static types tp_name is parsed at the last dot.
tp_basicsize and tp_itemsize drive allocation. tp_basicsize is the fixed byte size of one instance; tp_itemsize is the size of each additional trailing item for variable-length types (zero for fixed-size types). When you allocate an instance with n items, the allocator reserves tp_basicsize + n * tp_itemsize bytes. int has a non-zero tp_itemsize because a PyLongObject stores a variable number of 30-bit digits; float does not. These are exposed in Python as __basicsize__/__itemsize__:
>>> int.__basicsize__, int.__itemsize__
(24, 4)
>>> float.__basicsize__, float.__itemsize__
(24, 0)Lifecycle Slots — tp_new, tp_init, tp_alloc, tp_dealloc, tp_free
These five drive object creation and destruction; the full choreography is in Object Creation new init and alloc, so here is just what each field is. tp_alloc allocates raw, zeroed memory for an instance (default PyType_GenericAlloc). tp_new (__new__) is the constructor proper: it produces and returns a new, possibly-uninitialized object — it is a static method receiving the type as its first argument. tp_init (__init__) initializes an already-created instance in place and returns int (0 on success). On the way out, tp_dealloc is the destructor: it must drop references the object holds, then hand the memory back via tp_free (default PyObject_Free, or PyObject_GC_Del for GC-tracked types). tp_finalize (__del__) runs before deallocation and may run only once; it is the cycle-safe finalization hook (see Finalizers and the del Method). tp_del is a legacy slot kept for binary compatibility and effectively unused by modern code.
Behavior Slots — tp_repr, tp_hash, tp_call, tp_str, tp_richcompare
These scalar function pointers are the C implementations the interpreter calls for built-in operations on an instance. tp_repr backs repr(); tp_str backs str() (and defaults to tp_repr when absent). tp_hash backs hash() and returns a Py_hash_t; setting it to the sentinel PyObject_HashNotImplemented makes the type unhashable (this is what __hash__ = None compiles to). tp_call makes an instance callable — when it is non-NULL, obj(...) is legal and dispatches here. tp_richcompare implements all six ordering/equality operators in one function: it receives the two operands and an integer op-code (Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE) and returns the result (or Py_NotImplemented).
Each of these slots corresponds to a Python dunder (__repr__, __hash__, __call__, __str__, and the comparison family __lt__…__ge__). The bidirectional mechanism that keeps the C slot and the Python dunder synchronized — so that defining __repr__ in a Python class fills tp_repr, and a C type’s tp_repr is exposed as a __repr__ slot wrapper — is the subject of Type Slots and the Protocol Tables.
Attribute and Iteration Slots — tp_getattro, tp_setattro, tp_iter, tp_iternext, tp_descr_get, tp_descr_set
tp_getattro and tp_setattro implement attribute access (obj.attr read and write/delete). The defaults are PyObject_GenericGetAttr / PyObject_GenericSetAttr, which run the full descriptor + MRO + instance-dict lookup protocol detailed in Attribute Lookup Mechanics. (The older tp_getattr/tp_setattr take a char* name and are deprecated; the o suffix means “object name.”) tp_iter (__iter__) returns an iterator; tp_iternext (__next__) advances one and signals exhaustion by returning NULL with StopIteration set — together they are the The Iterator Protocol. tp_descr_get/tp_descr_set make instances of the type behave as Descriptors when stored on a class.
The three sub-struct pointers tp_as_number, tp_as_sequence, tp_as_mapping (plus tp_as_async, tp_as_buffer) hang the protocol tables off the type — a + b, seq[i], mapping[key]. Each is NULL unless the type participates in that protocol. Their contents and the dispatch machinery live entirely in Type Slots and the Protocol Tables.
Inheritance and Introspection — tp_base, tp_bases, tp_mro, tp_dict
tp_base is the single primary base (a C pointer); tp_bases is the full tuple of bases (__bases__); tp_mro is the linearized method resolution order tuple (__mro__), computed by C3 linearization (see Method Resolution Order and C3 Linearization). tp_dict is the type’s namespace (__dict__) — where the class body’s methods, the slot-wrapper descriptors, and class variables live. Crucially, tp_base/tp_bases/tp_mro/tp_dict are not set by the static C initializer; they are filled in by PyType_Ready() at startup (for static types) or by type creation (for heap types). The static PyTypeObject literal an extension author writes only sets tp_base if it differs from object, and leaves tp_mro/tp_dict as NULL.
tp_methods/tp_members/tp_getset are static C arrays (PyMethodDef, PyMemberDef, PyGetSetDef) that PyType_Ready() converts into descriptors and installs into tp_dict. tp_subclasses, tp_cache, tp_weaklist are internal bookkeeping. tp_dictoffset and tp_weaklistoffset record where in an instance the per-instance __dict__ and weakref list live — but in 3.14 these are increasingly replaced by the managed-layout flags (below), and tp_weaklistoffset is documented as deprecated in favor of Py_TPFLAGS_MANAGED_WEAKREF.
GC and Versioning — tp_traverse, tp_clear, tp_version_tag
A type whose instances can be part of a reference cycle must set Py_TPFLAGS_HAVE_GC and supply tp_traverse (visits every owned PyObject* member via the Py_VISIT() macro, so the cyclic collector can find cycles) and tp_clear (breaks a cycle by dropping those references with Py_CLEAR()). These are how the The Cyclic Garbage Collector sees inside an object. tp_version_tag is a per-type cache-validity counter: the attribute-lookup cache (and the specializing interpreter’s inline caches, see Inline Caches and Quickening) tag their entries with it; any change to the type bumps the tag via PyType_Modified(), invalidating stale cached lookups. tp_watched/tp_versions_used support the type-watcher mechanism the specializing interpreter uses.
tp_flags — The Bitmask That Describes the Type
tp_flags is a single unsigned long carrying boolean facts about the type that the interpreter checks on hot paths. The bit values below are from Include/object.h (CPython 3.14.0) — they are stable across releases (the docs guarantee individual bits do not move), which lets Py_LIMITED_API extensions read them safely.
| Flag | Bit | Meaning |
|---|---|---|
Py_TPFLAGS_INLINE_VALUES | 1 << 2 | Instances store attribute values inline after the object header (3.13+); implies GC |
Py_TPFLAGS_MANAGED_WEAKREF | 1 << 3 | VM manages the weakref list (replaces tp_weaklistoffset) |
Py_TPFLAGS_MANAGED_DICT | 1 << 4 | VM manages __dict__; sets tp_dictoffset automatically; implies GC |
Py_TPFLAGS_DISALLOW_INSTANTIATION | 1 << 7 | tp_new is NULL; the type cannot be instantiated directly |
Py_TPFLAGS_IMMUTABLETYPE | 1 << 8 | Type attributes cannot be set or deleted |
Py_TPFLAGS_HEAPTYPE | 1 << 9 | Type was allocated on the heap (created at runtime) |
Py_TPFLAGS_BASETYPE | 1 << 10 | Type may be subclassed; if clear, it is “final” |
Py_TPFLAGS_HAVE_VECTORCALL | 1 << 11 | Type supports the fast vectorcall calling protocol |
Py_TPFLAGS_READY | 1 << 12 | PyType_Ready() has finished initializing this type |
Py_TPFLAGS_READYING | 1 << 13 | PyType_Ready() is currently running on this type |
Py_TPFLAGS_HAVE_GC | 1 << 14 | Instances participate in cyclic GC; requires tp_traverse/tp_clear |
Py_TPFLAGS_LONG_SUBCLASS | 1 << 24 | Instances are int subclasses (fast PyLong_Check) |
The *_SUBCLASS flags (Py_TPFLAGS_LONG_SUBCLASS, LIST_SUBCLASS, TUPLE_SUBCLASS, BYTES_SUBCLASS, UNICODE_SUBCLASS, DICT_SUBCLASS, BASE_EXC_SUBCLASS, TYPE_SUBCLASS) are a clever optimization: instead of walking the MRO to answer “is this object a str?”, PyUnicode_Check just tests one bit via PyType_FastSubclass(). Every subclass of str inherits the UNICODE_SUBCLASS bit, so the check is O(1).
Py_TPFLAGS_DEFAULT is the baseline value an extension should OR into its tp_flags; it carries the always-on bits the runtime expects. A typical static type writes tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE.
Static Types vs. Heap Types
This is the most consequential distinction among type objects. A static type is a PyTypeObject struct written as a C global, compiled into the interpreter or an extension, and finalized once by PyType_Ready(). A heap type is a PyTypeObject (actually a larger PyHeapTypeObject) allocated on the heap at runtime — exactly what the class statement produces, and what type(name, bases, dict) produces.
The defining bit is Py_TPFLAGS_HEAPTYPE. It is set automatically for runtime-created types and clear for static ones, and the consequences are sharp:
>>> bool(int.__flags__ & (1 << 9)) # int is a static type
False
>>> class C: pass
>>> bool(C.__flags__ & (1 << 9)) # a class-statement type is a heap type
True
>>> int.foo = 1 # static types are immutable
Traceback (most recent call last):
TypeError: cannot set 'foo' attribute of immutable type 'int'
>>> C.foo = 1 # heap types are mutable
>>> C.foo
1Static types get Py_TPFLAGS_IMMUTABLETYPE set by PyType_Ready(), which is why you cannot monkey-patch int — the error message even names the flag’s effect (“immutable type”). Static types are also effectively immortal: they are never deallocated, their refcount does not track their instances, and they exist for the whole process. Heap types, by contrast, are ordinary reference-counted objects: an instance holds a reference to its heap type (ob_type is a counted reference for heap types), so the type stays alive as long as instances do, and is collected when nothing references it.
Static types are faster and simpler but rigid (you write them in C, they cannot be subclassed in C-incompatible ways, they cannot be GC’d). The modern guidance from the C API docs is to prefer heap types for new extension types, because they integrate cleanly with subinterpreters and per-module state. Heap types are created with PyType_FromSpec and friends, described next.
Creating Types in C — PyType_FromSpec and PyType_Ready
For a static type, the extension author fills in a PyTypeObject literal and calls PyType_Ready(&MyType) once during module init. PyType_Ready does the heavy finishing work: it sets tp_base to object if unset, computes tp_bases and the C3 tp_mro, inherits slots from the base type, builds tp_dict from tp_methods/tp_members/tp_getset and from the type’s own filled slots (via add_operators, see Type Slots and the Protocol Tables), and sets Py_TPFLAGS_READY.
For a heap type, the modern path is PyType_FromSpec(PyType_Spec *spec) (and the more general PyType_FromMetaclass(metaclass, module, spec, bases), added in 3.12, which lets you pin a custom metaclass and associate per-module state). A PyType_Spec is a compact description:
typedef struct {
const char *name; /* dotted type name → tp_name */
int basicsize; /* → tp_basicsize (negative = "extra beyond base", see below) */
int itemsize; /* → tp_itemsize */
unsigned int flags; /* Py_TPFLAGS_* (HEAPTYPE added automatically) */
PyType_Slot *slots; /* {slot_id, function_pointer}, terminated by {0, NULL} */
} PyType_Spec;Each PyType_Slot names a field by a Py_* slot ID and supplies its value: Py_tp_init, Py_tp_dealloc, Py_nb_add (a number slot), Py_sq_length (a sequence slot), Py_mp_subscript (a mapping slot), and so on. The FromSpec path cannot set the internal fields (tp_dict, tp_mro, tp_cache, tp_subclasses, tp_weaklist) and steers you toward flags instead of raw offsets — e.g. you set Py_TPFLAGS_MANAGED_DICT rather than tp_dictoffset, and Py_TPFLAGS_MANAGED_WEAKREF rather than tp_weaklistoffset. The function allocates a PyHeapTypeObject, copies the spec into it, runs PyType_Ready internally, and returns a new reference.
/* A minimal heap type created from a spec. */
static PyType_Slot Counter_slots[] = {
{Py_tp_doc, "A trivial counter."}, /* → tp_doc */
{Py_tp_new, PyType_GenericNew}, /* → tp_new */
{Py_tp_init, (initproc)Counter_init}, /* → tp_init */
{Py_tp_dealloc, (destructor)Counter_dealloc},/* → tp_dealloc */
{Py_nb_add, (binaryfunc)Counter_add}, /* → as_number.nb_add (a protocol slot) */
{0, NULL}, /* sentinel */
};
static PyType_Spec Counter_spec = {
.name = "mymod.Counter",
.basicsize = sizeof(CounterObject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.slots = Counter_slots,
};
/* In module init: */
PyObject *Counter = PyType_FromSpec(&Counter_spec); /* runs PyType_Ready, returns new ref */A few version-sensitive refinements, all verified against the c-api/type.html reference (3.14.5 docs): a negative PyType_Spec.basicsize means “this many extra bytes beyond the base type’s instance size” (retrieved at runtime via PyObject_GetTypeData) — the docs note “Changed in version 3.12: Previously, this field could not be negative.” New in 3.14 are Py_tp_token/Py_TP_USE_SPEC (a static-layout identity token for safe cross-extension subclass checks, both “Added in version 3.14.”) and PyType_Freeze() (“Added in version 3.14.”), which sets Py_TPFLAGS_IMMUTABLETYPE on a heap type to make it immutable like a built-in.
Common Misunderstandings
“A type is a class object, like a dict of methods.” No — a type is a struct of C function-pointer slots plus a tp_dict. The dunder methods you see in Type.__dict__ are descriptors that, for built-in operations, are kept in sync with the C slots. The slot is the truth on the hot path; the dict entry is the introspectable face. (See Type Slots and the Protocol Tables.)
“type(x) and x.__class__ are the same.” Usually, but not guaranteed: type(x) reads the C ob_type header directly, whereas x.__class__ is an attribute lookup that a class can override. For built-ins they agree; for proxy objects they can diverge.
“I can just memcpy a PyTypeObject to clone a type.” No — tp_dict, tp_mro, tp_subclasses and the readiness flags are interlinked runtime state. Cloning a type means going through PyType_FromSpec/type(), not copying bytes.
“Setting tp_basicsize smaller than the base type is fine.” It is not — a subtype’s instances must be layout-compatible with the base. tp_basicsize must be at least the base’s, and variable-length subclassing has further restrictions enforced by PyType_Ready.
See Also
- PyObject and the Object Header — the universal header every object (including a type) starts with
- Type Slots and the Protocol Tables — the
tp_as_number/tp_as_sequence/tp_as_mappingsub-structs and the slot↔dunder sync machinery - Object Creation new init and alloc — the full
tp_new/tp_init/tp_alloclifecycle - Attribute Lookup Mechanics — what
tp_getattro(defaultPyObject_GenericGetAttr) actually does - Method Resolution Order and C3 Linearization — how
tp_mrois computed - The type and object Relationship — why
type(type) is typeand howobject/typebootstrap - Metaclasses — customizing the type of a type
- Immortal Objects — why static types are never deallocated
- Python Internals MOC — §4 The Object Model