CPython's object Implementation in C

The PyObject and PyVarObject layouts, reference counting, the PyBaseObject_Type table, and the default tp methods (repr, str, hash, richcompare, new/init, dealloc, attribute access, __sizeof__) traced through the CPython 3.15 source.

Every Python value is a PyObject *. The type that defines what that pointer points at is object itself, implemented in CPython as PyBaseObject_Type in Objects/typeobject.c. Its instances are the smallest possible Python objects: just a reference count and a type pointer. This article traces that representation and the default behavior that every other type inherits from it.

The PyObject struct

The layout is defined in Include/object.h. In a normal (non-free-threaded) build with 64-bit pointers, it is:

c
// Include/object.h
struct _object {
    _Py_ANONYMOUS union {
#if SIZEOF_VOID_P > 4
        int64_t ob_refcnt_full;
        struct {
#  if PY_BIG_ENDIAN
            uint16_t ob_flags;
            uint16_t ob_overflow;
            uint32_t ob_refcnt;
#  else
            uint32_t ob_refcnt;
            uint16_t ob_overflow;
            uint16_t ob_flags;
#  endif
        };
#else
        Py_ssize_t ob_refcnt;
#endif
        _Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, char) _aligner;
    };
    PyTypeObject *ob_type;
};

Two fields matter. ob_refcnt is the reference count; ob_type is the pointer to the object’s type. PyObject_HEAD expands to PyObject ob_base;, so every concrete struct (PyLongObject, PyFloatObject, …) begins with exactly these fields, which is what makes the cast between PyObject * and a more specific pointer valid.

The reference count is manipulated by Py_INCREF/Py_DECREF. Py_DECREF calls the type’s tp_dealloc when the count reaches zero. New objects get their count set to one by _Py_NewReference, which PyObject_Init (and the inline _PyObject_Init) calls after storing the type pointer:

c
// Include/internal/pycore_object.h
static inline void
_PyObject_Init(PyObject *op, PyTypeObject *typeobj)
{
    assert(op != NULL);
    Py_SET_TYPE(op, typeobj);          // op->ob_type = typeobj
    _Py_INCREF_TYPE(typeobj);
    _Py_NewReference(op);              // sets ob_refcnt to 1
}

Immortal objects (such as None, True, False, and static types) use a sentinel refcount (_Py_IMMORTAL_INITIAL_REFCNT) so Py_INCREF/Py_DECREF become no-ops for them.

Identity is just pointer equality:

c
// Include/object.h
#define Py_Is(x, y) ((x) == (y))

and id(obj) in Python returns that pointer value (cast to an integer).

ob_size for variable-size objects

Objects whose size is determined at allocation time carry an extra ob_size field. These are PyVarObjects:

c
// Include/object.h
struct PyVarObject {
    PyObject ob_base;
    Py_ssize_t ob_size; // Number of items in variable part
};
#define PyObject_VAR_HEAD  PyVarObject ob_base;

ob_size is an element count, not a byte count. Types such as list, tuple, bytes and str start with PyObject_VAR_HEAD and allocate tp_basicsize + nitems * tp_itemsize bytes via _PyObject_NewVar:

c
// Objects/object.c
PyVarObject *
_PyObject_NewVar(PyTypeObject *tp, Py_ssize_t nitems)
{
    PyVarObject *op;
    const size_t size = _PyObject_VAR_SIZE(tp, nitems);
    op = (PyVarObject *) PyObject_Malloc(size);
    if (op == NULL) {
        return (PyVarObject *) PyErr_NoMemory();
    }
    _PyObject_InitVar(op, tp, nitems);   // sets ob_size = nitems
    return op;
}

_PyObject_InitVar stores the count with Py_SET_SIZE, and Py_SIZE(ob) reads it back (with an assertion that the object is not an int/bool, whose ob_size is overloaded for the sign/magnitude representation).

PyBaseObject_Type

The base type table is in Objects/typeobject.c:

c
// Objects/typeobject.c
PyTypeObject PyBaseObject_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "object",
    sizeof(PyObject),                           /* tp_basicsize */
    0,                                          /* tp_itemsize */
    object_dealloc,                             /* tp_dealloc */
    0,                                          /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    object_repr,                                /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    PyObject_GenericHash,                       /* tp_hash */
    0,                                          /* tp_call */
    object_str,                                 /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    PyObject_GenericSetAttr,                    /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
    object_doc,
    0,                                          /* tp_traverse */
    0,                                          /* tp_clear */
    object_richcompare,                         /* tp_richcompare */
    ...
    object_methods,                             /* tp_methods */
    0,                                          /* tp_members */
    object_getsets,                             /* tp_getset */
    0,                                          /* tp_base */
    ...
    0,                                          /* tp_dictoffset */
    object_init,                                /* tp_init */
    PyType_GenericAlloc,                        /* tp_alloc */
    object_new,                                 /* tp_new */
    PyObject_Free,                              /* tp_free */
};

Key facts: tp_basicsize is sizeof(PyObject) (16 bytes on 64-bit), tp_itemsize is 0 (fixed size), there is no tp_base (it is the root), and Py_TPFLAGS_BASETYPE allows subclassing. Because tp_dictoffset is zero, a plain object() instance has no instance dictionary and cannot be given attributes.

Default behaviors

Allocation: object_new

c
// Objects/typeobject.c
static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    if (excess_args(args, kwds)) {
        if (type->tp_new != object_new) {
            /* subclass overrode __new__: complain here */
            ...
        }
        if (type->tp_init == object_init) {
            PyErr_Format(PyExc_TypeError, "%.200s() takes no arguments",
                         type->tp_name);
            return NULL;
        }
    }
    if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
        /* error: can't instantiate abstract class */
        ...
    }
    PyObject *obj = type->tp_alloc(type, 0);   // PyType_GenericAlloc
    if (obj == NULL) {
        return NULL;
    }
    return obj;
}

So object() accepts no arguments; a subclass that overrides neither __new__ nor __init__ gets “takes no arguments”. Abstract classes (those with unimplemented abstract methods) are rejected here.

Initialization: object_init

c
// Objects/typeobject.c
static int
object_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    PyTypeObject *type = Py_TYPE(self);
    if (excess_args(args, kwds)) {
        if (type->tp_init != object_init) {
            PyErr_SetString(PyExc_TypeError,
                            "object.__init__() takes exactly one argument ...");
            return -1;
        }
        if (type->tp_new == object_new) {
            /* __new__ not overridden: complain */
            ...
        }
    }
    return 0;
}

The default __init__ is a no-op; it only enforces the argument rules.

Deallocation: object_dealloc

c
// Objects/typeobject.c
static void
object_dealloc(PyObject *self)
{
    Py_TYPE(self)->tp_free(self);   // PyObject_Free for base object
}

repr: object_repr

c
// Objects/typeobject.c
static PyObject *
object_repr(PyObject *self)
{
    PyTypeObject *type = Py_TYPE(self);
    PyObject *mod = type_module(type);
    ...
    PyObject *name = type_qualname((PyObject *)type, NULL);
    if (mod != NULL && !_PyUnicode_Equal(mod, &_Py_ID(builtins)))
        rtn = PyUnicode_FromFormat("<%U.%U object at %p>", mod, name, self);
    else
        rtn = PyUnicode_FromFormat("<%s object at %p>", type->tp_name, self);
    ...
    return rtn;
}

A plain instance prints as <object object at 0x...>; types outside builtins are qualified with their module name.

str: object_str

c
// Objects/typeobject.c
static PyObject *
object_str(PyObject *self)
{
    unaryfunc f = Py_TYPE(self)->tp_repr;
    if (f == NULL)
        f = object_repr;
    return f(self);
}

Unless a subclass overrides __repr__/__str__, str(obj) falls back to repr(obj).

Hash: PyObject_GenericHash

c
// Python/pyhash.c
Py_hash_t
PyObject_GenericHash(PyObject *obj)
{
    return Py_HashPointer(obj);
}

Py_hash_t
Py_HashPointer(const void *ptr)
{
    Py_hash_t hash = _Py_HashPointerRaw(ptr);
    if (hash == -1) {
        hash = -2;
    }
    return hash;
}

The default hash is derived from the object’s address, so two distinct instances have different hashes and object is hashable. Subclasses that define __eq__ typically set tp_hash = PyObject_HashNotImplemented (or override) to stay consistent.

Rich comparison: object_richcompare

c
// Objects/typeobject.c
static PyObject *
object_richcompare(PyObject *self, PyObject *other, int op)
{
    switch (op) {
    case Py_EQ:
        res = Py_NewRef((self == other) ? Py_True : Py_NotImplemented);
        break;
    case Py_NE:
        if (Py_TYPE(self)->tp_richcompare == NULL) {
            res = Py_NewRef(Py_NotImplemented);
            break;
        }
        res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
        if (res != NULL && res != Py_NotImplemented) {
            int ok = PyObject_IsTrue(res);
            Py_DECREF(res);
            res = ok ? Py_NewRef(Py_False) : Py_NewRef(Py_True);
        }
        break;
    default:
        res = Py_NewRef(Py_NotImplemented);
        break;
    }
    return res;
}

__eq__ returns True only for the exact same object, otherwise NotImplemented (letting the other operand or the default identity rule decide). __ne__ delegates to __eq__ and inverts. Ordering operators return NotImplemented, so <, >, etc. raise TypeError by default.

Attribute access: PyObject_GenericGetAttr / PyObject_GenericSetAttr

c
// Objects/object.c
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}

The generic getter looks up descriptors on the type, then the instance __dict__ (if tp_dictoffset is set). Because object has no tp_dictoffset, instances of object itself have no attributes at all; obj.x = 1 raises AttributeError. Subclasses that need attributes get a dict offset (or managed dicts) from the type machinery.

The one attribute always available is __class__, provided by the object_getsets table:

c
// Objects/typeobject.c
static PyGetSetDef object_getsets[] = {
    {"__class__", object_get_class, object_set_class,
     PyDoc_STR("the object's class")},
    {0}
};

__sizeof__

c
// Objects/typeobject.c
static PyObject *
object___sizeof___impl(PyObject *self)
{
    Py_ssize_t res, isize;
    res = 0;
    isize = Py_TYPE(self)->tp_itemsize;
    if (isize > 0) {
        res = _PyVarObject_CAST(self)->ob_size * isize;
    }
    res += Py_TYPE(self)->tp_basicsize;
    return PyLong_FromSsize_t(res);
}

So sys.getsizeof(obj) reports tp_basicsize + ob_size * tp_itemsize, i.e. the allocated size of the instance.

Summary

object is the root of the hierarchy: a 16-byte (on 64-bit) struct holding only ob_refcnt and ob_type, with ob_size added for variable-size subtypes. PyBaseObject_Type fills in sensible defaults – identity-based equality and hashing, a pointer-based repr, generic attribute access, no-op init, abstract-class checking in __new__, and PyObject_Free as the deallocator – that every other type either inherits or overrides. All of Python’s object model rests on these two fields and this one type table.

PythonCpythonInternalsObject