Skip to content

7. The Python Object Model

How every value in Python is a heap-allocated object, and the role of type objects in defining behavior.

The Python object model is the foundation of CPython. Everything that runs in Python eventually becomes an operation on objects: integers, strings, lists, modules, functions, classes, exceptions, frames, and even compiled code.

At the language level, Python says every object has an identity, a type, and a value. Object identity stays fixed after creation, is compares identity, and id() returns an integer representing that identity. (Python documentation)

CPython implements this model with C structures, object headers, reference counts, type objects, and operation slots.

7.1 Object, Value, and Identity

A Python object has three core properties.

PropertyMeaningExample
IdentityThe object’s stable identityid(x)
TypeThe object’s runtime typetype(x)
ValueThe data represented by the object42, "abc", [1, 2]

Example:

x = [1, 2, 3]
y = x

print(x is y)        # True
print(type(x))       # <class 'list'>
print(x)             # [1, 2, 3]

x and y are two names bound to the same object. They have the same identity.

x.append(4)
print(y)             # [1, 2, 3, 4]

The list changed. The binding did not create a copy.

In CPython, object identity is closely tied to object address for normal objects. The language only promises a stable identity, not that identity must be a memory address.

7.2 Names Bind to Objects

Python variables are bindings, not storage boxes.

This code:

a = 10
b = a

does not copy the integer value into b. It binds b to the same object referenced by a.

For immutable objects this often feels like value copying:

a = 10
b = a
a = 20

print(b)             # 10

But the object 10 did not change. The name a was rebound to another object.

For mutable objects the difference is visible:

a = []
b = a

a.append("x")

print(b)             # ['x']

The name a and the name b refer to the same list. Mutating through one reference is visible through the other.

This name-to-object model is central to CPython. Bytecode instructions mostly load object references, store object references, pass object references, and call operations on object references.

7.3 Every Runtime Value Is a PyObject *

Inside CPython, objects are normally handled through pointers of type PyObject *. The official C API documentation states that every pointer to a Python object can be cast to PyObject *, and that normal release builds store a reference count and a pointer to the corresponding type object in the base object structure. (Python documentation)

Conceptually:

typedef struct {
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

This is simplified, but it captures the essential model.

Every object begins with a shared header:

+--------------------+
| reference count    |
+--------------------+
| type pointer       |
+--------------------+
| object-specific    |
| payload            |
+--------------------+

For an integer, the payload stores integer digits.

For a list, the payload stores size information and a pointer to an array of element references.

For a function, the payload stores a code object, globals, defaults, closure cells, and related metadata.

The common header lets the interpreter treat all objects uniformly at the top level.

7.4 Fixed-Size and Variable-Size Objects

CPython distinguishes fixed-size objects and variable-size objects.

Fixed-size objects use the base object header.

Variable-size objects include an additional size field. The C API documentation describes PyVarObject as an extension of PyObject that adds an ob_size field for variable-sized objects. (Python documentation)

Conceptually:

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size;
} PyVarObject;

A tuple is variable-sized because a tuple of length 2 and a tuple of length 100 need different storage.

A bytes object is variable-sized.

A long integer is also variable-sized because CPython stores arbitrary-precision integers using a sequence of digits.

Approximate shape:

PyObject
    used by many fixed-size objects

PyVarObject
    used by objects whose logical size is known at allocation time

Examples:

Object kindHeader kindReason
floatPyObjectFixed-size payload
tuplePyVarObjectNumber of elements varies
bytesPyVarObjectNumber of bytes varies
intPyVarObjectNumber of integer digits varies
strSpecialized variable layoutString length varies

7.5 Object Types Define Behavior

An object’s type determines what operations it supports. Python’s data model defines this at the language level: objects have types, and types determine supported operations. (Python documentation)

At the CPython level, the type pointer in the object header points to a PyTypeObject.

Conceptually:

object
    ob_refcnt
    ob_type  -------->  type object
                         name
                         size
                         base classes
                         method table
                         number slots
                         sequence slots
                         mapping slots
                         call slot
                         attribute slots

When Python evaluates:

x + y

CPython does not search for a free-standing add function. It asks the relevant type machinery how addition works for those objects.

For integers, addition uses integer-specific code.

For strings, addition uses string concatenation.

For lists, addition creates a concatenated list.

For user-defined classes, addition may call __add__.

The syntax is uniform. The implementation is type-directed.

7.6 Types Are Objects

A type is itself an object.

print(type(42))      # <class 'int'>
print(type(int))     # <class 'type'>
print(type(type))    # <class 'type'>

This recursive structure is intentional.

int is an object.

list is an object.

User-defined classes are objects.

The type of most type objects is type.

class User:
    pass

print(type(User))    # <class 'type'>
print(type(User()))  # <class '__main__.User'>

This explains why classes can be assigned, passed, stored, decorated, and created dynamically.

def make_class():
    class Item:
        pass
    return Item

C = make_class()
obj = C()

A class statement creates a class object. It binds the class name to that object.

7.7 Object Layout and Type Layout

A CPython object’s layout must match what its type object expects.

The CPython source comments in Include/object.h note that objects are accessed through PyObject *, and object sizes do not change after allocation because moving or resizing objects would require updating references. (GitHub)

That constraint is important.

A list can grow, but the list object itself does not expand in place to hold all elements directly. Instead, it owns a separate element array that can be reallocated.

A tuple cannot grow. Its item references are stored as part of the tuple allocation.

Conceptually:

list object
    header
    current length
    allocated capacity
    pointer to item array  ---> [PyObject*, PyObject*, PyObject*, ...]

tuple object
    header
    length
    inline item references ---> [PyObject*, PyObject*, PyObject*, ...]

This difference explains why list append can be amortized efficient while tuple size is fixed.

7.8 Mutability

Mutability means an object’s value can change while its identity remains the same.

xs = [1, 2]
before = id(xs)

xs.append(3)
after = id(xs)

print(before == after)   # True

The list object remains the same object. Its contents changed.

Immutable objects do not expose operations that change their value in place.

s = "abc"
t = s.upper()

print(s)    # abc
print(t)    # ABC

The string operation returns another object.

Common mutable and immutable objects:

MutableImmutable
listint
dictfloat
setstr
bytearraybytes
most class instancestuple, if its contained references are fixed

A tuple is immutable as a container, but it can contain mutable objects:

t = ([],)
t[0].append(1)

print(t)    # ([1],)

The tuple still points to the same list. The list changed.

7.9 Reference Semantics

Most CPython runtime operations move references to objects, not objects themselves.

Function calls pass object references.

def add_item(xs):
    xs.append(1)

items = []
add_item(items)

print(items)     # [1]

The function receives a reference to the same list object.

Rebinding a local name does not affect the caller:

def replace(xs):
    xs = [1, 2, 3]

items = []
replace(items)

print(items)     # []

The name xs inside the function was rebound. The original list was not mutated.

This distinction matters for API design. Mutating an object and rebinding a local variable are different operations.

7.10 Attributes

Objects can expose attributes.

class User:
    pass

u = User()
u.name = "Ada"

print(u.name)

For ordinary user-defined objects, instance attributes are usually stored in an instance dictionary.

Conceptually:

u
    type pointer ---> User
    __dict__ -----> {"name": "Ada"}

Attribute lookup is more complex than a direct dictionary lookup. CPython must account for:

data descriptors
instance dictionary
non-data descriptors
class attributes
base classes
__getattribute__
__getattr__
method binding

Example:

class User:
    species = "human"

u = User()
u.name = "Ada"

print(u.name)       # instance attribute
print(u.species)    # class attribute

If the attribute is absent from the instance, CPython searches the class and its bases.

7.11 Methods Are Descriptors

A function stored on a class behaves like a method when accessed through an instance.

class Counter:
    def inc(self):
        return 1

c = Counter()

print(c.inc)

c.inc is a bound method. It combines the function object with the instance c.

This behavior comes from the descriptor protocol.

A descriptor is an object that defines one or more of:

__get__(self, obj, objtype=None)
__set__(self, obj, value)
__delete__(self, obj)

Functions implement __get__, so they bind automatically when retrieved from an instance.

Conceptual lookup:

Counter.inc
    raw function object

c.inc
    bound method:
        function = Counter.inc
        self = c

Descriptors are a key part of the object model. They implement methods, properties, class methods, static methods, slots, and many extension-level attributes.

7.12 Special Methods and Slots

Python syntax maps to special methods.

SyntaxSpecial method
x + y__add__
x[i]__getitem__
x()__call__
len(x)__len__
iter(x)__iter__
next(x)__next__
x in y__contains__
str(x)__str__
repr(x)__repr__

At the CPython level, many of these operations correspond to slots in PyTypeObject.

For example, a type may provide number slots, sequence slots, mapping slots, and call slots.

This means Python code like:

len(obj)

does not simply call obj.__len__() through normal instance lookup. CPython uses type-level protocol machinery. This makes common operations faster and more consistent.

7.13 Built-in Types Are Ordinary Objects With Privileged Implementations

Built-in types participate in the same object model, but their implementations live in C.

print(type([]))          # <class 'list'>
print(type({}))          # <class 'dict'>
print(type("abc"))       # <class 'str'>

The official built-in types documentation groups principal built-in types as numerics, sequences, mappings, classes, instances, and exceptions. (Python documentation)

The difference between list and a user-defined class is not that one is an object and the other is not. Both are objects. The difference is that list has a C implementation with a fixed internal layout and specialized slots.

A Python list object contains implementation details such as:

object header
logical length
allocated capacity
pointer to element storage

A dict contains a hash table implementation.

A string contains Unicode-specific layout and cached metadata.

These internal layouts are optimized for CPython’s runtime behavior.

7.14 Classes and Instances

A class defines behavior shared by its instances.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(10, 20)

At runtime:

Point
    class object
    type: type
    attributes:
        __init__
        ...

p
    instance object
    type: Point
    attributes:
        x = 10
        y = 20

Calling the class invokes construction machinery:

Point(10, 20)
    call type object
    allocate instance through __new__
    initialize instance through __init__
    return instance

This is why construction can be customized:

class OnlyOne:
    def __new__(cls):
        print("allocate")
        return super().__new__(cls)

    def __init__(self):
        print("initialize")

__new__ creates or returns an object. __init__ initializes it.

7.15 Inheritance and Method Resolution

Python supports inheritance through classes.

class Animal:
    def speak(self):
        return "?"

class Dog(Animal):
    def speak(self):
        return "woof"

When resolving an attribute on an instance, CPython searches according to the class method resolution order, usually called MRO.

print(Dog.__mro__)

For single inheritance:

Dog
Animal
object

For multiple inheritance, Python uses C3 linearization.

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__)

The MRO gives a single ordered path for attribute lookup. It prevents ambiguous repeated traversal of shared base classes.

7.16 Attribute Lookup Order

For a normal expression:

obj.name

CPython performs a structured lookup.

Simplified order:

1. Look for data descriptors on the type or its bases.
2. Look in the instance dictionary.
3. Look for non-data descriptors or other class attributes.
4. If still missing, call __getattr__ if defined.
5. Otherwise raise AttributeError.

A data descriptor defines __set__ or __delete__.

A non-data descriptor defines only __get__.

This explains why property can override an instance dictionary entry:

class User:
    @property
    def name(self):
        return "computed"

u = User()
print(u.name)

The property is a data descriptor. It wins over normal instance storage.

7.17 Object Creation

Object creation usually has two stages.

allocation
    reserve memory for the object

initialization
    set the initial object state

At Python level:

obj = cls.__new__(cls)
cls.__init__(obj)

In normal code, calling the class performs both steps.

At C level, the type object controls allocation and initialization through slots such as:

tp_new
tp_init
tp_alloc
tp_dealloc

This separation matters because immutable objects need their value during creation.

For example, a tuple or int cannot be created empty and then freely mutated into its final value through public operations. Their final value is established during allocation or construction.

7.18 Object Destruction

Object destruction in CPython is mostly reference-count driven.

When an object’s reference count reaches zero, CPython calls its deallocation function.

Conceptually:

Py_DECREF(obj)
    decrement reference count
    if reference count == 0:
        call type-specific deallocator

The deallocator releases references owned by the object, frees auxiliary memory, and returns the object’s memory to the allocator.

For containers, deallocation can cascade:

xs = [[1], [2], [3]]
del xs

Deleting the outer list decrements references to the inner lists. If those inner lists have no other references, they are also destroyed.

Cycles require cyclic garbage collection, since reference counts alone cannot reclaim objects that keep each other alive.

7.19 Equality and Identity

Identity and equality are different.

a = [1, 2]
b = [1, 2]

print(a == b)    # True
print(a is b)    # False

== asks whether objects compare equal.

is asks whether two references point to the same object.

At the CPython level:

is
    compare object pointers

==
    dispatch rich comparison through type machinery

Custom classes can define equality:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return (
            isinstance(other, Point)
            and self.x == other.x
            and self.y == other.y
        )

Identity remains independent of equality.

7.20 Hashing

Hashing supports dictionary keys and set elements.

An object used as a dict key must have a stable hash while it remains in the dictionary.

d = {}
d["name"] = "Ada"

Strings are hashable because they are immutable.

Lists are not hashable because their contents can change:

hash("abc")      # works
hash((1, 2))     # works
hash([1, 2])     # TypeError

A custom class can define:

__hash__
__eq__

The rule is practical: if equality can change, the hash table can break. Mutable objects should generally avoid value-based hashing.

7.21 Containers Store References

Python containers store references to objects.

xs = [object(), object(), object()]

The list stores references to three objects. It does not inline their complete object data.

Conceptually:

list
    items[0] ---> object A
    items[1] ---> object B
    items[2] ---> object C

This explains shallow copy behavior:

a = [[1], [2]]
b = a.copy()

b[0].append(99)

print(a)     # [[1, 99], [2]]

The outer list was copied. The inner list objects were shared.

A deep copy recursively copies contained objects:

import copy

a = [[1], [2]]
b = copy.deepcopy(a)

7.22 Object Protocols

Python relies on protocols rather than explicit interfaces.

An object participates in a protocol by implementing the right methods.

Iteration protocol:

class Count:
    def __init__(self, stop):
        self.current = 0
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

Container protocol:

class Bag:
    def __init__(self):
        self.items = []

    def __contains__(self, item):
        return item in self.items

Context manager protocol:

class Resource:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False

These protocols allow user-defined objects to work with syntax such as:

for x in obj:
    ...

with obj:
    ...

if x in obj:
    ...

The object model is the bridge between syntax and behavior.

7.23 A Useful Internal Model

A Python expression such as:

result = obj.method(x) + y

can be read as object-model operations:

load obj
look up attribute "method"
bind method to obj if descriptor rules apply
load x
call bound method
load y
perform binary addition through type slots
bind result name to returned object

Every step moves or creates object references.

Every operation is mediated by type information.

Every result is another object reference.

7.24 Minimal C-Level Sketch

A simplified extension type begins with a CPython object header.

typedef struct {
    PyObject_HEAD
    long value;
} CounterObject;

The PyObject_HEAD macro supplies the standard object header required for CPython to treat this memory as a Python object. The type object then describes how this object behaves.

static PyTypeObject CounterType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "example.Counter",
    .tp_basicsize = sizeof(CounterObject),
    .tp_flags = Py_TPFLAGS_DEFAULT,
};

Real extension types need more fields, initialization, methods, error handling, module setup, and reference ownership. But the shape is the same:

instance memory starts with object header
type object describes behavior
runtime manipulates the object through PyObject *

7.25 Summary

The Python object model says every value has identity, type, and value. CPython realizes this model with PyObject * pointers, object headers, reference counts, type objects, and operation slots.

A name binds to an object. A container stores references to objects. A type defines behavior. A class is an object. An instance points to its class. Syntax such as addition, calls, indexing, iteration, and attribute access is implemented through type-directed protocol machinery.

This model is the basis for the rest of CPython: memory management, bytecode execution, attribute lookup, function calls, classes, descriptors, extension modules, and the C API.