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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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
// 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
// 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
// Objects/typeobject.c
static void
object_dealloc(PyObject *self)
{
Py_TYPE(self)->tp_free(self); // PyObject_Free for base object
}repr: object_repr
// 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
// 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
// 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
// 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
// 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:
// Objects/typeobject.c
static PyGetSetDef object_getsets[] = {
{"__class__", object_get_class, object_set_class,
PyDoc_STR("the object's class")},
{0}
};__sizeof__
// 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.