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.

Summarized by x-ai/grok-4.1-fast via openrouter

5293 input / 1238 output tokens in 13529ms

© 2026 Edge