Uniform Object Architecture: PyObject & PyVarObject
Explore the internal C layout of Python objects: PyObject headers, reference counting fields, PyVarObject containers, and the PyTypeObject slot vector dispatch system.
In CPython, everything is an object. Functions, modules, classes, integers, strings, and stack frames are all represented internally as heap-allocated C structures adhering to a uniform interface.
This structural uniformity allows the virtual machine to pass, inspect, and evaluate arbitrary data types using generic pointers (PyObject*), providing universal dynamic typing and seamless introspection.
8 bytes for ob_refcnt + 8 bytes for ob_type pointer on standard 64-bit platforms.
1. The Anatomy of PyObject & PyVarObject#
Every CPython object begins with a mandatory header structure:
Mathematical Memory Footprint#
Let be the set of all heap-allocated objects. The memory footprint for fixed-size versus variable-length objects is formalized as:
Where:
- bytes on 64-bit systems ( bytes for
ob_refcnt+ bytes forob_type). - bytes on 64-bit systems (adding bytes for
ob_size). - represents the container item count.
- represents stored item pointers (8 bytes per
PyObject*). - represent type-specific internal bookkeeping fields.
2. Inspecting Object Headers in C and Python#
Compare how the C definition translates to Python memory addresses using the standard library ctypes module:
1234567891011// CPython definition from Include/object.h typedef struct _object { _PyObject_HEAD_EXTRA // Double-linked list pointers in debug builds Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject; typedef struct { PyObject ob_base; Py_ssize_t ob_size; // Number of items in variable-length objects } PyVarObject;
3. Dynamic Dispatch & The Type Slot System#
When Python executes an expression like a + b or len(container), how does the interpreter resolve which C function to run?
CPython does not perform slow string-based method dictionary lookups for fundamental operations. Instead, it inspects function pointers stored in dedicated memory slots within the object's PyTypeObject:
Major Type Slot Tables#
The PyTypeObject struct contains specific sub-tables grouping related operations:
| Slot Table | Category | Key Operations | Example Functions |
|---|---|---|---|
tp_as_number | Arithmetic | +, -, *, /, //, %, ** | nb_add, nb_multiply, nb_power |
tp_as_sequence | Linear Containers | len(), x in s, s[i], concatenation | sq_length, sq_item, sq_concat |
tp_as_mapping | Key-Value Lookups | m[key], len(m), assignment | mp_subscript, mp_ass_subscript |
tp_as_async | Asynchronous Protocol | await, async for, async with | am_await, am_aiter, am_anext |
list implements both tp_as_sequence (enabling index slicing and concatenation with +) and tp_as_mapping (enabling subscript reading and assignment via mp_subscript).4. The Object Lifecycle: Allocation to Deallocation#
Step 1: Memory Allocation via `tp_alloc`
When an object is requested, CPython invokes the type's tp_alloc function. For standard Python objects, this requests a block of memory from the specialized small-object allocator (PyMalloc).
Step 2: Header Initialization
The newly allocated memory block has its ob_refcnt set to 1, and its ob_type pointer bound to the target class PyTypeObject. If it is a PyVarObject, ob_size is initialized to the requested element count.
Step 3: Instance Initialization via `tp_init`
The instance __init__ logic executes, populating user-defined instance attributes.
Step 4: Finalization & Deallocation via `tp_dealloc`
When ob_refcnt drops to zero, CPython immediately calls the type's tp_dealloc function. This releases nested references, cleans up external resources, and returns the memory block to the PyMalloc pool.
5. Concept Verification#
Test your understanding of CPython uniform object architecture:
What are the two foundational fields present in the header of every PyObject in CPython?
How does CPython execute operations like addition (a + b) with high performance?