bigonotes
DSA: Python & HacksPython Internals

Python Object Model

How Python names point at objects, what every object carries in its header, and why reassignment never changes other names.

3 min readLast updated Python Internals

In C, int a = 5; reserves a box called a and puts 5 in it. Python works differently: a = 5 makes (or finds) an object 5 somewhere in memory and attaches the name a to it. Python has names, not boxes. Once that idea clicks, aliasing bugs, mutable default arguments and backtracking code all make sense.

Every object starts with the same header#

In CPython (the standard interpreter), every value is a C struct that begins with a PyObject header. Ints, strings, lists, functions and modules all have it.

Field What it holds Why it matters
ob_refcnt How many references point at this object When it drops to 0, the object is freed immediately
ob_type Pointer to the object's type (int, str, list, ...) How Python knows what operations are allowed
payload The actual data (digits, characters, item pointers) Stored right after the header

Variable-size objects (int, str, tuple, list) use PyVarObject, which adds an ob_size field. That stored size is why len() is O(1)O(1) for built-in containers.

Names, assignment and identity#

Assignment never copies an object. It only points a name at one.

import sys

a = [1, 2, 3]
# no copy: b is a second name for the same list
b = a

print(hex(id(a)) == hex(id(b)))   # True: same address
print(a is b)                      # True: identity check
# usually 3: a, b, and getrefcount's own argument
print(sys.getrefcount(a))
  • id(x) gives the object's identity (its address in CPython).
  • a is b asks "same object?". a == b asks "equal values?".
  • Use is only for singletons such as None: write if x is None, not if x == None.

The reassignment gotcha#

What does this print?

x = [10, 20, 30]
y = x
x = [99, 100]
print(y)   # [10, 20, 30]

y still prints [10, 20, 30]. Here's why, step by step:

  1. x = [10, 20, 30] creates a list (say at 0x123) and points x at it.
  2. y = x points y at the same object, 0x123.
  3. x = [99, 100] creates a new list at 0x456 and moves x there. y never moved.
Names x and y: x is rebound to a new list while y still points to the original list object, each with a refcount, type and payload

Compare that with mutation, which changes the shared object itself:

x = [10, 20, 30]
y = x
x.append(40)       # mutates the object both names point at
print(y)           # [10, 20, 30, 40]

Rule of thumb: name = ... rebinds one name. Calling a method like .append() or assigning x[i] = ... changes the object, and every name pointing at it sees the change.

Function arguments work the same way#

Python passes references to objects ("call by sharing"). A function can mutate an object you pass in, but rebinding the parameter has no effect outside.

def mutate(nums):
    nums.append(4)      # caller sees this

def rebind(nums):
    nums = [0]          # only the local name moves

data = [1, 2, 3]
mutate(data)
rebind(data)
print(data)             # [1, 2, 3, 4]

When objects are freed#

CPython frees objects in two ways.

  • Reference counting: when ob_refcnt hits 0, CPython frees the object right away.
  • Cycle collector: objects that reference each other (e.g. two nodes pointing at each other) never reach 0 on their own. A separate garbage collector finds and frees these unreachable cycles.
  • An object with zero names left is unreachable, so it will be cleaned up.

Key takeaways#

  • Python variables are names attached to objects. Assignment never copies.
  • Every object carries a refcount and a type pointer before its data.
  • is checks identity, == checks value. Use is for None.
  • Rebinding a name affects only that name. Mutating an object affects every name that points at it.
  • Functions receive references, so they can mutate your arguments but cannot rebind your variables.

Found a mistake? Tell me on LinkedIn ↗

Further reading#