Python Variables: Sticky Notes on Shared Objects
Forget 'pass-by-reference'—Python variables are labels binding to objects via 'call by sharing'. Mutable defaults like [] create shared state across calls, causing ghost bugs; fix by using None and instantiating inside functions.
Names Bind to Objects, Not Values
Python lacks variables as fixed memory boxes like in C or Java. Instead, everything is an object in memory, and variables are labels (sticky notes) pointing to those objects. Assigning x = [1, 2, 3] creates a list object (e.g., at address 0x1234) and binds the name x to it. Passing to a function creates a new local label bound to the same object—no copy occurs.
This 'call by sharing' means mutations to mutable objects (lists, dicts) affect all bound names, but rebinding a local name inside a function doesn't alter the caller's object.
mutate example:
def mutate(lst):
lst.append(99) # Mutates shared object
x = [10, 20]
mutate(x)
# x now [10, 20, 99]—original object changed
rebind example:
def rebind(lst):
lst = [1, 2, 3] # Local label now points to new object
x = [10, 20, 99]
rebind(x)
# x unchanged—original object untouched
Rebinding peels the local label off the shared object and attaches it to a new one, leaving external references intact.
Mutable Defaults Create Permanent Shared State
Functions are objects with defaults evaluated once at definition time, stored in .__defaults__. A mutable default like items=[] creates one list object bound to the function forever—GC can't reclaim it while the function lives.
Buggy example:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]—mutates function's default
print(add_item(2, [])) # [2]—uses new list
print(add_item("a")) # [1, 'a']—reuses mutated default
First call mutates the shared default list. Later calls without items reuse it, accumulating data across invocations. In servers or workers, this leaks state between requests/jobs, manifesting as ghost bugs like User B seeing User A's data.
Defensive Fix: None + Instantiation Prevents Shared State
Replace mutable defaults with None, then create fresh objects inside the function at call time:
def add_item(item, items=None):
if items is None:
items = [] # New list per call
items.append(item)
return items
None is immutable/safe. Instantiation happens on the heap each run, ensuring no shared state. Enforce via linters like Flake8's B006 banning mutable defaults. This model resolves 90% of Python's 'weirdness' for production code.