Skip to content

12. Object Layout and Type Slots

PyTypeObject slot layout, tp_* function pointers, and how type slots encode object behavior for the interpreter.

CPython represents every runtime value as an object. Each object has a memory layout, and each object’s type describes how that memory should be interpreted.

The object layout answers:

What fields exist inside this object?
Where are the references to other Python objects?
How large is one instance?
Does the object have variable-sized trailing storage?
Does the object participate in cyclic garbage collection?

The type slots answer:

How is this object called?
How is it deallocated?
How does attribute lookup work?
How does indexing work?
How does addition work?
How does iteration work?
How is it represented as text?

Together, object layout and type slots form the bridge between Python-level behavior and C-level implementation.

12.1 Object Memory Starts With a Header

Every normal CPython object begins with an object header.

Conceptually:

typedef struct {
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

This gives every object two fundamental fields:

FieldMeaning
ob_refcntReference count
ob_typePointer to the object’s type

A concrete object places its own fields after this header.

Example shape:

PyLongObject
    PyObject header
    integer-specific fields

PyListObject
    PyObject / PyVarObject header
    list-specific fields

PyFunctionObject
    PyObject header
    function-specific fields

Because every object starts with the same header, generic CPython code can manipulate unknown objects through PyObject *.

12.2 Fixed-Size Object Layout

A fixed-size object has the same C struct size for every instance.

Example:

typedef struct {
    PyObject_HEAD
    double value;
} FloatLikeObject;

The memory shape:

+--------------------+
| ob_refcnt          |
+--------------------+
| ob_type            |
+--------------------+
| value              |
+--------------------+

All instances of this type have the same size.

Examples of mostly fixed-size object structs:

float
function
module
cell
method
weakref
many iterator objects
many descriptor objects

Fixed-size does not mean the object has no references to external storage. A function object has a fixed-size struct, but it points to a code object, globals dictionary, defaults tuple, closure tuple, annotations, and other objects.

12.3 Variable-Size Object Layout

A variable-size object extends the common header with a size field.

Conceptually:

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size;
} PyVarObject;

Extension types use:

typedef struct {
    PyObject_VAR_HEAD
    PyObject *items[1];
} ArrayLikeObject;

The memory shape:

+--------------------+
| ob_refcnt          |
+--------------------+
| ob_type            |
+--------------------+
| ob_size            |
+--------------------+
| variable payload   |
+--------------------+

ob_size has type-specific meaning.

TypeMeaning of size field
tupleNumber of elements
bytesNumber of bytes
intNumber and sign of internal digits
custom variable typeDefined by the type implementation

ob_size does not mean total memory size in bytes.

12.4 Inline Storage vs Indirect Storage

Variable-sized Python values can store data inline or indirectly.

A tuple stores item references inline in the same allocation:

tuple object
    header
    size = 3
    item[0] ---> object A
    item[1] ---> object B
    item[2] ---> object C

A list stores a pointer to a separate item array:

list object
    header
    size = 3
    ob_item ----+
    allocated   |
                v
              [ptr A][ptr B][ptr C][spare...]

This difference is fundamental.

A tuple cannot change size after allocation, so inline storage is efficient.

A list must grow and shrink, so it keeps elements in a separate array that can be reallocated without moving the list object itself.

12.5 Type Objects Describe Instance Layout

Each object points to a type object.

The type object stores layout metadata such as:

tp_basicsize
tp_itemsize
tp_dictoffset
tp_weaklistoffset
tp_flags

Important fields:

FieldMeaning
tp_basicsizeFixed size of an instance
tp_itemsizeSize of each variable trailing item
tp_dictoffsetOffset of instance __dict__, if any
tp_weaklistoffsetOffset of weakref list, if supported
tp_flagsType flags describing capabilities

For a fixed-size type:

tp_basicsize = sizeof(MyObject)
tp_itemsize = 0

For a variable-size type:

tp_basicsize = fixed header and fields
tp_itemsize = size of one trailing element

Allocation then computes:

total bytes = tp_basicsize + n * tp_itemsize

This is how CPython allocates a tuple of length n in one memory block.

12.6 Type Objects Describe Behavior

A type object also stores behavior.

A simplified view:

PyTypeObject
    name
    size fields
    base type
    method table
    member table
    getset table
    deallocator
    repr function
    call function
    attribute functions
    numeric slots
    sequence slots
    mapping slots
    iterator slots

When Python evaluates:

x + y

CPython dispatches through type slots.

When Python evaluates:

x[i]

CPython uses sequence or mapping slots.

When Python evaluates:

x()

CPython uses the call slot.

A type object is both a layout descriptor and a dispatch table.

12.7 The Main Type Slot Categories

PyTypeObject contains many fields. The important categories are:

Slot categoryPurpose
Lifecycle slotsallocation, initialization, deallocation
Representation slotsrepr, str
Attribute slotsget, set, descriptor behavior
Call slotfunction-call syntax
Number slotsarithmetic and bit operations
Sequence slotslength, indexing, containment, concatenation
Mapping slotsdictionary-style lookup and assignment
Iterator slotsiteration protocol
Buffer slotsraw memory exposure
GC slotstraversal and clearing
Subclassing slotsinheritance and MRO behavior

Each category maps Python syntax or runtime behavior to C function pointers.

12.8 Lifecycle Slots

Lifecycle slots control creation and destruction.

Important slots:

tp_new
tp_init
tp_alloc
tp_dealloc
tp_free

Typical creation path:

call class
    tp_new allocates or returns object
    tp_init initializes object
    return object

Example:

obj = MyClass(1, 2)

Conceptually:

MyClass.__call__
    MyClass.__new__
    MyClass.__init__

At C level, this flows through type slots.

For immutable objects, tp_new often constructs the full value because the object cannot be modified after creation.

For mutable objects, tp_init can fill or reset fields after allocation.

12.9 Deallocation Slot

tp_dealloc destroys an object whose reference count reached zero.

A simple deallocator:

static void
Box_dealloc(BoxObject *self)
{
    Py_XDECREF(self->value);
    Py_TYPE(self)->tp_free((PyObject *)self);
}

The deallocator must release every Python reference owned by the object.

For GC-aware types, the deallocator must also untrack the object before breaking references:

static void
Box_dealloc(BoxObject *self)
{
    PyObject_GC_UnTrack(self);
    Py_CLEAR(self->value);
    Py_TYPE(self)->tp_free((PyObject *)self);
}

A deallocator must be written defensively. Py_DECREF and Py_CLEAR can execute arbitrary Python code indirectly through finalizers.

12.10 Representation Slots

Representation slots support repr() and str().

Important slots:

tp_repr
tp_str

Python code:

repr(obj)
str(obj)

maps to type behavior.

Example type implementation sketch:

static PyObject *
Counter_repr(CounterObject *self)
{
    return PyUnicode_FromFormat("Counter(%ld)", self->value);
}

Then:

repr(counter)

can produce:

Counter(10)

If tp_str is absent, CPython may fall back to tp_repr or generic object formatting behavior depending on the type.

12.11 Attribute Access Slots

Attribute access is one of CPython’s most important paths.

Relevant slots:

tp_getattro
tp_setattro
tp_getattr
tp_setattr

Modern types usually use tp_getattro and tp_setattro.

Python code:

obj.name
obj.name = value
del obj.name

flows through attribute machinery.

For ordinary objects, generic attribute lookup handles:

data descriptors
instance dictionary
non-data descriptors
class attributes
base classes
__getattr__

A custom type can override this behavior by providing custom attribute slots.

12.12 Descriptor Slots

Descriptors implement binding behavior.

A descriptor type can define:

tp_descr_get
tp_descr_set

Python-level equivalents:

__get__
__set__
__delete__

Functions are descriptors. When a function is stored on a class and accessed through an instance, descriptor logic creates a bound method.

class Counter:
    def inc(self):
        return 1

c = Counter()
m = c.inc

Conceptually:

look up inc on Counter
find function object
function descriptor binds self = c
return bound method

Descriptors implement methods, properties, class methods, static methods, slots, and many built-in attributes.

12.13 Call Slot

The call slot supports function-call syntax.

Relevant slot:

tp_call

Python code:

obj(a, b, c)

requires the object’s type to be callable.

Functions, classes, methods, built-in functions, and objects with __call__ all participate in this protocol.

For a user-defined class:

class F:
    def __call__(self, x):
        return x + 1

f = F()
print(f(10))

At the type level, the class machinery ensures instances with __call__ behave as callable objects.

12.14 Number Slots

Number slots handle arithmetic and bitwise operations.

They live in a PyNumberMethods table.

Common conceptual slots:

nb_add
nb_subtract
nb_multiply
nb_remainder
nb_power
nb_negative
nb_positive
nb_absolute
nb_bool
nb_invert
nb_lshift
nb_rshift
nb_and
nb_xor
nb_or
nb_int
nb_float
nb_index

Python syntax:

a + b
a - b
a * b
-a
bool(a)
a & b
a << b

uses number protocol machinery.

For user-defined classes, special methods such as __add__, __bool__, and __index__ are connected to these slots.

12.15 Binary Operation Dispatch

Binary operations need careful dispatch because two operands have two types.

For:

a + b

CPython may consider:

left operand type
right operand type
subclass relationships
left slot
right reflected slot
NotImplemented result

Python-level methods:

__add__
__radd__
__iadd__

map to internal binary operation machinery.

A type can return NotImplemented to let the other operand participate.

Example:

class A:
    def __add__(self, other):
        return NotImplemented

class B:
    def __radd__(self, other):
        return "handled by B"

print(A() + B())

This dispatch behavior is part of Python’s data model and is implemented through type slots.

12.16 Sequence Slots

Sequence slots live in a PySequenceMethods table.

Common conceptual slots:

sq_length
sq_concat
sq_repeat
sq_item
sq_ass_item
sq_contains
sq_inplace_concat
sq_inplace_repeat

Python syntax:

len(x)
x[i]
x[i] = value
item in x
x + y
x * n

may use sequence slots.

Lists, tuples, strings, bytes, and ranges all expose sequence behavior, though their internal layouts differ.

12.17 Mapping Slots

Mapping slots live in a PyMappingMethods table.

Common conceptual slots:

mp_length
mp_subscript
mp_ass_subscript

Python syntax:

len(x)
x[key]
x[key] = value
del x[key]

may use mapping slots.

Dictionaries are the main mapping type.

A user-defined object that implements __getitem__, __setitem__, and __len__ can participate in mapping-like behavior through type slot integration.

12.18 Sequence vs Mapping Lookup

The same syntax can mean sequence or mapping access:

x[i]

For a list, i is an index.

For a dict, i is a key.

The type decides how to interpret the operation.

Conceptually:

list.__getitem__(index)
    index must be integer-like

dict.__getitem__(key)
    key may be any hashable object

At C level, CPython dispatches based on the type’s mapping and sequence slots.

12.19 Iterator Slots

Iteration uses type-level protocol support.

Relevant slots:

tp_iter
tp_iternext

Python code:

for item in obj:
    ...

roughly means:

iterator = iter(obj)
while true:
    try:
        item = next(iterator)
    except StopIteration:
        break

At C level:

tp_iter
    returns iterator object

tp_iternext
    returns next item or signals StopIteration

An iterator returns itself from tp_iter.

A container returns a separate iterator object.

12.20 Buffer Slots

The buffer protocol exposes raw memory to other objects without copying.

Relevant slot area:

tp_as_buffer

Objects such as bytes, bytearray, memoryview, arrays, and numerical extension types can participate.

The buffer protocol matters for:

zero-copy I/O
binary parsing
NumPy interop
memoryview
file and socket operations
serialization

A buffer exporter must keep memory valid while consumers hold views.

This makes buffer slots part of both the object model and memory lifetime model.

12.21 Garbage Collection Slots

GC-aware container types need traversal and clearing support.

Relevant slots:

tp_traverse
tp_clear

tp_traverse reports contained Python references to the collector.

static int
Box_traverse(BoxObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->value);
    return 0;
}

tp_clear breaks contained references during cyclic collection.

static int
Box_clear(BoxObject *self)
{
    Py_CLEAR(self->value);
    return 0;
}

If a type can participate in cycles and does not implement these correctly, it can leak unreachable cycles.

12.22 Member Tables

Some C extension fields can be exposed through member definitions.

Conceptual pattern:

static PyMemberDef Point_members[] = {
    {"x", T_LONG, offsetof(PointObject, x), 0, "x coordinate"},
    {"y", T_LONG, offsetof(PointObject, y), 0, "y coordinate"},
    {NULL}
};

The type object points to this table.

.tp_members = Point_members

This allows CPython to expose C struct fields as Python attributes.

Member tables are useful for simple C fields. More complex attributes usually use getset descriptors.

12.23 Getset Tables

Getset tables expose computed attributes.

Conceptual pattern:

static PyObject *
Point_get_norm(PointObject *self, void *closure)
{
    double norm = sqrt(self->x * self->x + self->y * self->y);
    return PyFloat_FromDouble(norm);
}

static PyGetSetDef Point_getset[] = {
    {"norm", (getter)Point_get_norm, NULL, "vector norm", NULL},
    {NULL}
};

The type object points to the getset table:

.tp_getset = Point_getset

A getset entry behaves like a descriptor.

Python code:

p.norm

calls the getter.

12.24 Method Tables

C extension methods are exposed through method tables.

Conceptual pattern:

static PyObject *
Counter_inc(CounterObject *self, PyObject *Py_UNUSED(ignored))
{
    self->value += 1;
    return PyLong_FromLong(self->value);
}

static PyMethodDef Counter_methods[] = {
    {"inc", (PyCFunction)Counter_inc, METH_NOARGS, "increment counter"},
    {NULL}
};

The type object points to the method table:

.tp_methods = Counter_methods

Python code:

counter.inc()

uses descriptor and method-call machinery to invoke the C function.

12.25 Heap Types and Static Types

CPython has static types and heap types.

Static types are defined as C global structures. Many built-in types historically use static type objects.

Heap types are allocated dynamically at runtime. User-defined Python classes are heap types.

Comparison:

Type kindAllocationTypical use
Static typeC static storageBuilt-in and extension-defined types
Heap typeRuntime allocationPython classes, many modern extension types

Heap types are more flexible. They support normal class behavior such as dynamic attributes, subclassing metadata, and runtime-managed lifecycle.

Modern extension code often prefers heap types through module definition slots and PyType_FromSpec.

12.26 PyType_FromSpec

PyType_FromSpec creates a type object from a declarative specification.

Conceptual sketch:

static PyType_Slot Counter_slots[] = {
    {Py_tp_dealloc, Counter_dealloc},
    {Py_tp_methods, Counter_methods},
    {0, NULL}
};

static PyType_Spec Counter_spec = {
    .name = "example.Counter",
    .basicsize = sizeof(CounterObject),
    .itemsize = 0,
    .flags = Py_TPFLAGS_DEFAULT,
    .slots = Counter_slots,
};

Then:

PyObject *type = PyType_FromSpec(&Counter_spec);

This style avoids directly initializing a large PyTypeObject struct and works better with evolving CPython internals.

12.27 Slot Inheritance

Subclasses inherit behavior from base classes unless they override it.

class A:
    def __len__(self):
        return 10

class B(A):
    pass

print(len(B()))

B inherits length behavior from A.

At the C level, type readiness fills inherited slots and computes the final type layout and method resolution metadata.

Slot inheritance must respect:

base class layout
method resolution order
descriptor behavior
special method lookup
type flags
GC support
memory offsets

This is why type creation is a substantial runtime operation.

12.28 Special Method Lookup

Special method lookup often bypasses normal instance lookup.

Example:

class X:
    pass

x = X()
x.__len__ = lambda: 3

len(x)

This still raises TypeError because len(x) looks for length behavior on the type, not in the instance dictionary.

Correct:

class X:
    def __len__(self):
        return 3

x = X()
print(len(x))

This rule lets CPython optimize protocol operations and preserves consistent behavior for built-in syntax.

12.29 Layout Conflicts

Multiple inheritance can create layout conflicts.

If two base classes require incompatible C-level instance layouts, CPython may reject the class.

Example shape:

BaseA requires C layout A
BaseB requires C layout B
Derived(BaseA, BaseB)
    cannot combine both layouts safely

Pure Python classes are usually flexible because their instance state is dictionary-based or slot-based.

C extension types have stricter layout requirements.

This is one reason extension type design should be conservative about inheritance unless subclassing behavior is explicitly needed.

12.30 Mental Model

Use this model when reading type code:

object memory stores state
type object describes layout and behavior
slots map Python operations to C functions
protocols are implemented through slot tables
attribute lookup uses descriptors and dictionaries
numeric, sequence, and mapping syntax dispatch through specialized slots
GC-aware objects must expose references through traversal slots
deallocation must release owned references and free memory

A Python expression is usually a path through type slots.

result = obj[key] + other

Conceptually:

load obj
dispatch subscription through mapping or sequence slot
load other
dispatch addition through number slots
store result

12.31 Summary

Object layout defines the memory shape of a CPython object. Type slots define what operations that object supports.

PyObject and PyVarObject provide the common headers. Concrete structs add type-specific fields. PyTypeObject records size, offsets, flags, lifecycle hooks, attribute behavior, protocol slots, method tables, member tables, getset tables, and garbage collection support.

This design allows CPython to combine a uniform object representation with highly specialized implementations for built-in and extension types.