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;
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
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;
Fixed-length object (PyObject): Variable-length object (PyVarObject):
┌──────────────┐ ┌──────────────┐
│ ob_refcnt │ │ ob_refcnt │
├──────────────┤ ├──────────────┤
│ ob_type │ │ ob_type │
└──────────────┘ ├──────────────┤
│ ob_size │ ← element count
└──────────────┘
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;
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;
PyFloatObject (pi = 3.14):
┌──────────────┐
│ ob_refcnt │ = 1
├──────────────┤
│ ob_type │──▶ PyFloat_Type
├──────────────┤
│ ob_fval │ = 3.14
└──────────────┘
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;
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)
└──────────────┘
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 },
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:
- Different object types need different amounts of memory — where does Python get this information when creating an object?
- 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;
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
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)
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 */
// ...
};
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'>
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 */
// ...
};
Notice line 2: ob_type points to itself — PyType_Type. This matches Python's behavior:
>>> type.__class__
<class 'type'>
>>> type.__class__ is type
True
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 */
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);
}
// ...
}
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 */
};
ob_type points to PyType_Type — consistent with Python behavior:
>>> object.__class__
<class 'type'>
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
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..."
In summary:
- All instance objects (
pi,e,[1,2,3]) haveob_type→ their type object - All type objects (
float,list) haveob_type→PyType_Type(type) - All type objects have
tp_base→PyBaseObject_Type(object) by default -
PyType_Typehasob_type→ itself -
PyBaseObject_Typehastp_base→NULL(end of inheritance chain)
Top comments (0)