DEV Community

James Lee
James Lee

Posted on

Python Object Model: How CPython Represents Everything as an Object

In object-oriented theory, "class" and "object" are two fundamental concepts. In Python, both exist as objects internally. A "class" is an object — called a type object. An instance created from a class is also an object — called an instance object.

Objects can be further classified by their characteristics:

Category Description
Mutable object Can be modified after creation
Immutable object Cannot be modified after creation
Fixed-length object Size is fixed
Variable-length object Size is not fixed

So what does a Python object actually look like internally?

Since Python is implemented in C, a Python object is a C struct that organizes the memory the object occupies. Different types of objects have different data and behaviors — so it's reasonable to guess that different types are represented by different structs. But objects also share common traits — every object needs a reference count for garbage collection. So there must be a common header shared by all object structs.

Let's verify this by reading the source code.


PyObject — The Foundation of All Objects

In CPython, every object is represented by a PyObject struct, and an object reference is a PyObject * pointer. PyObject is defined in Include/object.h:

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
Enter fullscreen mode Exit fullscreen mode

Ignoring the _PyObject_HEAD_EXTRA macro for now, the struct contains two fields:

  • ob_refcnt — reference count
  • ob_type — pointer to the type object

Reference count: incremented when something references the object, decremented when the reference is released. When it reaches zero, the object is garbage collected — the simplest GC mechanism.

Type pointer: points to the object's type object, which describes the instance object's data layout and behavior.

_PyObject_HEAD_EXTRA

#ifdef Py_TRACE_REFS
#define _PyObject_HEAD_EXTRA    \
    struct _object *_ob_next;   \
    struct _object *_ob_prev;
#else
#define _PyObject_HEAD_EXTRA
#endif
Enter fullscreen mode Exit fullscreen mode

When Py_TRACE_REFS is defined, this macro expands to two pointers that form a doubly-linked list tracking all live heap objects. This is a debug feature, not enabled in normal builds.

PyVarObject — Variable-Length Objects

For variable-length objects, we need to add a length field on top of PyObject:

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
Enter fullscreen mode Exit fullscreen mode
Fixed-length object (PyObject):        Variable-length object (PyVarObject):
┌──────────────┐                       ┌──────────────┐
│  ob_refcnt   │                       │  ob_refcnt   │
├──────────────┤                       ├──────────────┤
│  ob_type     │                       │  ob_type     │
└──────────────┘                       ├──────────────┤
                                       │  ob_size     │  ← element count
                                       └──────────────┘
Enter fullscreen mode Exit fullscreen mode

Two convenience macros make it easy for other objects to embed these headers:

#define PyObject_HEAD      PyObject ob_base;
#define PyObject_VAR_HEAD  PyVarObject ob_base;
Enter fullscreen mode Exit fullscreen mode

Concrete Object Examples

PyFloatObject — Fixed-Length

A float has a fixed size: just a double on top of the PyObject header:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;
Enter fullscreen mode Exit fullscreen mode
PyFloatObject (pi = 3.14):
┌──────────────┐
│  ob_refcnt   │  = 1
├──────────────┤
│  ob_type     │──▶ PyFloat_Type
├──────────────┤
│  ob_fval     │  = 3.14
└──────────────┘
Enter fullscreen mode Exit fullscreen mode

PyListObject — Variable-Length

A list has a variable size — it uses a dynamic array to store element pointers:

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;
Enter fullscreen mode Exit fullscreen mode
PyListObject:
┌──────────────┐
│  ob_refcnt   │
├──────────────┤
│  ob_type     │──▶ PyList_Type
├──────────────┤
│  ob_size     │  = current length (e.g. 3)
├──────────────┤
│  ob_item     │──▶ [ *obj0 | *obj1 | *obj2 |  (empty)  ]
├──────────────┤         ↑ pointers to element objects
│  allocated   │  = current capacity (e.g. 4)
└──────────────┘
Enter fullscreen mode Exit fullscreen mode

Three key fields:

  • ob_item — pointer to the dynamic array storing element object pointers
  • allocated — total capacity of the dynamic array
  • ob_size — current number of elements (the list's length)

Object Initialization Macros

// For fixed-length objects: sets ob_refcnt=1, ob_type=type
#define PyObject_HEAD_INIT(type)        \
    { _PyObject_EXTRA_INIT              \
    1, type },

// For variable-length objects: also sets ob_size
#define PyVarObject_HEAD_INIT(type, size)   \
    { PyObject_HEAD_INIT(type) size },
Enter fullscreen mode Exit fullscreen mode

These macros appear frequently throughout CPython's source code.


PyTypeObject — The Foundation of All Types

PyObject gives us the common fields shared by all objects. But two questions remain unanswered:

  1. Different object types need different amounts of memory — where does Python get this information when creating an object?
  2. For a given object, how does Python know what operations it supports?

This meta-information belongs to the object's type, stored in a separate entity. The ob_type pointer in PyObject points right to it. PyTypeObject is defined in Include/object.h:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name;            /* type name, e.g. "float" */
    Py_ssize_t tp_basicsize;        /* memory size for instances */
    Py_ssize_t tp_itemsize;         /* for variable-length types */

    destructor tp_dealloc;
    printfunc  tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;

    // ...
    struct _typeobject *tp_base;    /* base class (inheritance) */
    // ...
} PyTypeObject;
Enter fullscreen mode Exit fullscreen mode

Key fields:

  • tp_name — type name (e.g. "float", "list")
  • tp_basicsize / tp_itemsize — memory layout for creating instances
  • tp_base — pointer to the base class type object
  • tp_dealloc, tp_repr, tp_getattr, etc. — function pointers for supported operations

PyTypeObject is the C-level representation of Python's "class" concept.


Type Object and Instance Object in Memory

>>> float
<class 'float'>
>>> pi = 3.14
>>> e = 2.71
>>> type(pi) is float
True
Enter fullscreen mode Exit fullscreen mode

There is exactly one float type object in the system, holding all meta-information about floats. There can be many float instance objects — pi, e, and countless others.

Memory layout:

┌─────────────────────────────────────────────────────────┐
│                   PyFloat_Type (type object)            │
│  ob_type ──▶ PyType_Type                                │
│  tp_name = "float"                                      │
│  tp_basicsize = sizeof(PyFloatObject)                   │
│  tp_repr = float_repr                                   │
│  ...                                                    │
└─────────────────────────────────────────────────────────┘
      ▲                    ▲
      │ ob_type            │ ob_type
┌─────┴──────┐       ┌─────┴──────┐
│ pi=3.14    │       │ e=2.71     │
│ ob_refcnt=1│       │ ob_refcnt=1│
│ ob_fval=   │       │ ob_fval=   │
│   3.14     │       │   2.71     │
└────────────┘       └────────────┘
(PyFloatObject)      (PyFloatObject)
Enter fullscreen mode Exit fullscreen mode

Note: float, pi, e are just pointers to the actual objects — not the objects themselves.

Since the float type object is globally unique, it's defined as a static global variable in C — PyFloat_Type in Objects/floatobject.c:

PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",                                    /* tp_name */
    sizeof(PyFloatObject),                      /* tp_basicsize */
    0,                                          /* tp_itemsize */
    (destructor)float_dealloc,                  /* tp_dealloc */
    // ...
    (reprfunc)float_repr,                       /* tp_repr */
    // ...
};
Enter fullscreen mode Exit fullscreen mode

Line 2 initializes ob_refcnt, ob_type (→ PyType_Type), and ob_size. Line 3 sets tp_name to "float".


PyType_Type — The Type of Types

Every type object is itself an object, so it also has a type. The type of float is type:

>>> float.__class__
<class 'type'>
>>> class Foo(object): pass
>>> Foo.__class__
<class 'type'>
Enter fullscreen mode Exit fullscreen mode

In C, type is represented by PyType_Type, defined in Objects/typeobject.c:

PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)   // ob_type points to itself!
    "type",                                   /* tp_name */
    sizeof(PyHeapTypeObject),                 /* tp_basicsize */
    sizeof(PyMemberDef),                      /* tp_itemsize */
    (destructor)type_dealloc,                 /* tp_dealloc */
    // ...
    (reprfunc)type_repr,                      /* tp_repr */
    // ...
};
Enter fullscreen mode Exit fullscreen mode

Notice line 2: ob_type points to itselfPyType_Type. This matches Python's behavior:

>>> type.__class__
<class 'type'>
>>> type.__class__ is type
True
Enter fullscreen mode Exit fullscreen mode

PyType_Type is the meta type — the type of all types. It's a crucial object in Python's type system, enabling advanced metaclass-based patterns.


PyBaseObject_Type — The Base of All Types

object is the base class of all types. Its C-level entity is PyBaseObject_Type.

You might expect to find it via PyFloat_Type.tp_base — but that field is left as 0 in the static definition:

0,   /* tp_base */
Enter fullscreen mode Exit fullscreen mode

The missing piece is PyType_Ready, called during interpreter initialization:

int
PyType_Ready(PyTypeObject *type)
{
    // ...
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        base = type->tp_base = &PyBaseObject_Type;  // default base = object
        Py_INCREF(base);
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

PyType_Ready finishes initializing all type objects — setting tp_base to PyBaseObject_Type for any type that doesn't explicitly specify a base.

PyBaseObject_Type is defined in Objects/typeobject.c:

PyTypeObject PyBaseObject_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "object",                                   /* tp_name */
    sizeof(PyObject),                           /* tp_basicsize */
    0,                                          /* tp_itemsize */
    object_dealloc,                             /* tp_dealloc */
    // ...
    object_repr,                                /* tp_repr */
};
Enter fullscreen mode Exit fullscreen mode

ob_type points to PyType_Type — consistent with Python behavior:

>>> object.__class__
<class 'type'>
Enter fullscreen mode Exit fullscreen mode

And tp_base is not set for PyBaseObject_Type — the inheritance chain must have a terminal node, otherwise attribute lookup would loop forever:

>>> print(object.__base__)
None
Enter fullscreen mode Exit fullscreen mode

The Complete Object Hierarchy

Now we can draw the full picture of CPython's object system:

┌─────────────────────────────────────────────────────────────────┐
│                    Complete Object Hierarchy                    │
│                                                                 │
│   Instances          Type Objects          Meta / Base          │
│                                                                 │
│  ┌────────┐         ┌────────────┐                             │
│  │ pi=3.14│─ob_type▶│ PyFloat_   │                             │
│  └────────┘         │ Type       │─tp_base──┐                  │
│  ┌────────┐         │ "float"    │          │                  │
│  │ e=2.71 │─ob_type▶│            │─ob_type─┐│                  │
│  └────────┘         └────────────┘         ││                  │
│                                            ││                  │
│  ┌────────┐         ┌────────────┐         ││                  │
│  │[1,2,3] │─ob_type▶│ PyList_    │         ││                  │
│  └────────┘         │ Type       │─tp_base─┤│                  │
│                     │ "list"     │─ob_type─┤│                  │
│                     └────────────┘         ││                  │
│                                            ││                  │
│                     ┌────────────┐         ││                  │
│                     │ PyType_    │◀────────┘│                  │
│                     │ Type       │─ob_type─▶(itself)           │
│                     │ "type"     │─tp_base──┤                  │
│                     └────────────┘          │                  │
│                                             ▼                  │
│                     ┌────────────────────────────┐             │
│                     │ PyBaseObject_Type "object" │             │
│                     │ ob_type ──▶ PyType_Type    │             │
│                     │ tp_base = NULL (terminal)  │             │
│                     └────────────────────────────┘             │
└─────────────────────────────────────────────────────────────────┘

Key relationships:
──ob_type──▶  "this object's type is..."
──tp_base──▶  "this type inherits from..."
Enter fullscreen mode Exit fullscreen mode

In summary:

  • All instance objects (pi, e, [1,2,3]) have ob_type → their type object
  • All type objects (float, list) have ob_typePyType_Type (type)
  • All type objects have tp_basePyBaseObject_Type (object) by default
  • PyType_Type has ob_type → itself
  • PyBaseObject_Type has tp_baseNULL (end of inheritance chain)

Top comments (0)