Python Object Model Explained

Learn how Python's object model works under the hood: what objects are, how names reference them, and why every value in Python is an object.

8 min read

The Python object model is the set of rules that define how data exists and behaves in a Python program. Every value you work with is an object. A number, a string, a function, a class, and even a module you import are all objects stored in memory with a type, an identity, and a value.

There are no primitive types that sit outside this system. In Python, there is no split between lightweight primitives and heavyweight wrapper objects the way Java has int and Integer. Everything follows the same rules.

The object model matters because it explains behaviour you encounter every day. When you pass a list to a function and the function modifies it, the caller sees the change. The article on Python variables and object references covers the practical side, but the underlying mechanism is the object model.

The three properties every object has

Every Python object carries three pieces of information tracked by the runtime for the object's entire lifetime. You can inspect all three from Python code and use them to understand how your program executes.

Identity: the object's unique address

An object's identity is a number that uniquely identifies it during its lifetime. No two objects alive at the same time share the same identity. On CPython this is literally the memory address, but the language specification only guarantees uniqueness.

The built-in id() function returns the identity. The is operator compares two identities and returns True only when both sides refer to the exact same object.

pythonpython
a = [1, 2, 3]
b = [1, 2, 3]
 
print(id(a))
print(id(b))
print(a is b)

Running this code shows that two lists with identical content have different identities. The is operator confirms they are distinct objects in separate memory locations.

texttext
4372336128
4372336192
False

The two lists are distinct objects at different memory locations. The is operator compares identity, not content. For value comparison, use == instead.

Python may cache small integers and short strings as an optimization, reusing the same object. This is an implementation detail you should not rely on for correctness.

Type: what the object can do

An object's type determines which operations are valid on it. You can inspect the type with the type() built-in. Once an object is created, its type never changes for the lifetime of that object.

pythonpython
print(type(42))
print(type("hello"))
print(type([1, 2, 3]))
print(type(print))

Each value returns a different type class from the Python runtime. Even the print function itself has a type, confirming that functions are also inspectable objects.

texttext
<class 'int'>
<class 'str'>
<class 'list'>
<class 'builtin_function_or_method'>

Asking len(42) fails not because the integer has a hidden length, but because the int type does not define a length operation. The article on the Python data model and magic methods explains how types define operations through special methods.

Value: the data the object holds

An object's value is the actual data it represents. For a number the value is the numeric quantity, for a string it is the sequence of characters, and for a list it is the collection of references to other objects.

pythonpython
x = 42
y = "hello"
z = [1, 2, 3]
 
print(x)
print(y)
print(z)

Each variable holds a reference to an object in memory. Printing each one displays the value that the referenced object represents.

texttext
42
hello
[1, 2, 3]

The value may or may not be changeable depending on whether the type is mutable. Immutable types like int and str cannot have their value changed after creation, while mutable types like list and dict can.

The article on Python object identity and mutability explores this distinction in depth.

Names are labels, not containers

In Python, a variable name does not hold an object. It is a label that points to an object. When you write x = 42, Python creates an integer object with the value 42 and attaches the name x to it.

When you later write y = x, you attach the name y to the same object. You do not copy the object. You create a second label for the same thing.

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

Both x and y point to the same list. The append call modifies that single object, and the change is visible through both names.

texttext
[1, 2, 3, 4]

This is not a bug. It is the object model working exactly as designed. When you reassign a name, you move the label to a different object rather than modifying the object itself.

pythonpython
x = [1, 2, 3]
y = x
x = [4, 5, 6]
 
print(y)

The name x now points to a new list, but y still points to the original one. The original list is unchanged.

texttext
[1, 2, 3]

Reassignment moves labels. It never copies objects or modifies the object the label previously pointed to. This distinction between rebinding a name and mutating an object is the source of most confusion around Python's argument-passing behaviour.

How Python manages object lifetime

Python tracks how many references point to each object using reference counting. When a name, a list element, a dictionary key, or any other container points to an object, the count increases. When a reference goes away, the count decreases.

The sys.getrefcount() function reveals the current reference count for any object. It includes a temporary reference from the function argument itself, so the count is always at least one higher than you might expect.

pythonpython
import sys
 
x = [1, 2, 3]
print(sys.getrefcount(x))
 
y = x
print(sys.getrefcount(x))
 
del y
print(sys.getrefcount(x))

Each change in the number of names pointing to the list is reflected in the reference count output. The count rises when a new name is bound to the object and falls again once that name is removed with del.

texttext
2
3
2

When the count reaches zero, Python knows the object is no longer reachable and frees its memory. For objects that reference each other in cycles, Python runs a cycle detector as part of its garbage collector to find and free unreachable groups of objects.

The type hierarchy

Every object's type is itself an instance of type. The type class is also an object whose type is type. This creates a closed loop at the top of the hierarchy, and it is why the built-in type() function itself has a type you can inspect.

pythonpython
print(type(42))
print(type(int))
print(type(type))

The integer 42 has type int. The int class itself is an object whose type is type. And type has type type, closing the loop.

texttext
<class 'int'>
<class 'type'>
<class 'type'>

Every class in Python ultimately inherits from object. You can verify this with isinstance() checks across the hierarchy, including built-in types, user-defined classes, and even type itself.

pythonpython
print(isinstance(42, int))
print(isinstance(42, object))
print(isinstance(int, type))
print(isinstance(int, object))
print(isinstance(type, object))

All five checks return True, confirming that everything traces back to object, whether it is a plain value, a built-in type, or the type metaclass sitting at the top of the hierarchy.

texttext
True
True
True
True
True

Every class is also an instance of type or a custom metaclass. This dual role, where a class is both a type for its instances and an instance of its metaclass, is central to Python's object model. Later articles on metaclasses and class creation explore how this works in detail.

How this affects everyday Python

The object model directly explains two behaviours that surprise many newcomers. The first is how function arguments work. A function parameter becomes a new name for the same object the caller passed.

If the function mutates a mutable object through that parameter, the caller sees the change. If the function reassigns the parameter name, only the local label moves. The caller's name is unaffected, since reassignment inside the function never touches the object the caller's name still points to.

pythonpython
def append_item(lst):
    lst.append("added")
    lst = ["new", "list"]
 
items = [1, 2, 3]
append_item(items)
print(items)

The append call modifies the shared list, so the caller sees the change. The reassignment only moves the local name, leaving the caller's original list object untouched and still holding the appended value.

texttext
[1, 2, 3, 'added']

The second surprise is default argument values. Default values are evaluated once at function definition time, not each time the function is called. When the default is a mutable object, every call that uses the default shares the same object.

pythonpython
def add_to_list(item, target=[]):
    target.append(item)
    return target
 
print(add_to_list(1))
print(add_to_list(2))

The same list object persists across calls, accumulating items from each invocation, because the default value was created only once when Python defined the function.

texttext
[1]
[1, 2]

The safe pattern is to use None as the default and create a new mutable object inside the function body. This is a direct application of the object model: the default expression runs once at definition time and the resulting object is shared.

Rune AI

Rune AI

Key Insights

  • Every value in Python is an object with three core properties: identity, type, and value.
  • Names in Python are labels that reference objects; they do not contain or own the data directly.
  • The type of an object determines what operations are valid on it and cannot change after the object is created.
  • Python uses reference counting and garbage collection to manage object lifetime automatically.
  • Understanding the object model makes argument passing, mutability, inheritance, and memory behaviour predictable.
RunePowered by Rune AI

Frequently Asked Questions

What is the Python object model?

The Python object model is the set of rules that govern how every value in Python is represented as an object in memory. It defines three core properties that every object has (identity, type, and value), how names reference objects rather than contain them, and how the type hierarchy connects built-in and user-defined types.

Why does Python say everything is an object?

Python means this literally. Integers, strings, functions, classes, modules, types, exceptions, and even the bytecode that runs your program are all objects. Each one has a unique identity, a type that determines what operations work on it, and a value. There are no primitive types that exist outside the object system.

Conclusion

The Python object model is the foundation every other Python concept rests on. Once you understand that names are labels pointing at objects, that every object carries identity, type, and value, and that the type determines what an object can do, mutable versus immutable behaviour, argument passing, inheritance, and memory management all become consequences of this single consistent model.