- TL;DR
- A name is a label, not a box
- Assignment copies the reference
- Reference count
- Function arguments are assignments
- The bit that surprises people
- References
TL;DR
Python has names, not variables (as in C++ and Java).
A Python object is stored in memory with names and references. Variables in Python are just the references to the objects in the memory.
A name is just a label for an object, so one object can have many names. A reference is a pointer that refers to an object.
Object has: type, value, reference count.
Assignment in Python never copies values. It only copies references. In assignment right side is evaluated first.
a = [1,2,3]
b = a
a = [4,5,6]
print(a) # [4, 5, 6]
print(b) # [1, 2, 3], Holds the original valueA name is a label, not a box
In C, int x = 5 reserves a box of memory called x and puts 5 in it. x = 7 overwrites the contents of that box.
In Python, x = 5 creates an int object somewhere in the heap and makes the name x point at it. x = 7 doesn't touch the original object at all — it repoints the label at a different one.
x = 5
print(id(x)) # e.g. 140712834567890
x = 7
print(id(x)) # different number — different object entirelyThe name lives in a namespace, which is essentially a dict mapping strings to object pointers. You can see it:
x = 5
globals()["x"] # 5Assignment copies the reference
a = [1, 2, 3]
b = a # not a copy of the list — a second label on the same list
b.append(4)
print(a) # [1, 2, 3, 4]
print(a is b) # True — same objectis compares identity (same object), == compares value. Two names, one object, refcount 2.
Now the crucial distinction:
b = [9, 9] # REBINDING: moves the label b, leaves a's object alone
print(a) # [1, 2, 3, 4]
b = a
b.clear() # MUTATING: changes the shared object
print(a) # [] — a sees it toob = ... on the bare name rebinds. b.append(...), b.clear(), b[0] = ... mutate. That's the whole difference, and it's exactly why I said use chunk = [] rather than chunk.clear() in the chunking generator — after yield chunk, the caller holds a reference to that same object, so clearing it reaches into their data.
Reference count
Every object tracks how many references point at it. When that hits zero, CPython frees it immediately.
import sys
a = [1, 2, 3]
sys.getrefcount(a) # 2 — one for `a`, one temporary for the argument itself
b = a
sys.getrefcount(a) # 3
del b # `del` removes a name, not an object
sys.getrefcount(a) # 2del a after that drops it to zero and the list is deallocated. A cycle detector handles objects that reference each other and would otherwise never reach zero.
Function arguments are assignments
Parameters are bound to the caller's objects by the same rule, so the mutate/rebind split applies inside functions too:
def rebind(lst):
lst = [0] # local name repointed; caller unaffected
def mutate(lst):
lst.append(0) # shared object changed; caller sees it
xs = [1]
rebind(xs); print(xs) # [1]
mutate(xs); print(xs) # [1, 0]The bit that surprises people
Small ints and short strings are cached and reused, so identity can look odd:
a = 256; b = 256; a is b # True — cached singleton
a = 257; b = 257; a is b # False in a REPL — two distinct objectsNothing special is happening; there just happens to be one shared 256 object. Never use is to compare values — reserve it for None, True, False, and sentinels.
The practical takeaway: immutable objects (int, str, tuple, frozenset) make the distinction invisible, since you can't mutate them and every operation rebinds. For lists, dicts, sets, and your own classes, "who else has a reference to this object" is a question you have to hold in your head.