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 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 basks "same object?".a == basks "equal values?".- Use
isonly for singletons such asNone: writeif x is None, notif 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:
x = [10, 20, 30]creates a list (say at0x123) and pointsxat it.y = xpointsyat the same object,0x123.x = [99, 100]creates a new list at0x456and movesxthere.ynever moved.
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_refcnthits 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.
ischecks identity,==checks value. UseisforNone.- 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.