Pass by Reference/Value
Python uses neither classic pass-by-value nor pass-by-reference. The correct term is pass by object reference (or "pass by assignment").
The Mental Model
- Every variable is a label (reference) pointing to an object.
- When you pass an argument, the function receives a copy of the reference, not the object itself.
Immutable Objects (int, str, tuple, float…)
The reference is copied. Rebinding inside the function has no effect outside.
def try_change(x):
x = 100 # rebinds local x — original is untouched
n = 42
try_change(n)
print(n) # still 42
Mutable Objects (list, dict, set, custom objects…)
The reference is copied, but both labels point to the same object. Mutations are visible outside.
def append_item(lst):
lst.append(99) # mutates the same object
numbers = [1, 2, 3]
append_item(numbers)
print(numbers) # [1, 2, 3, 99]
But rebinding the name inside the function still has no effect:
def try_replace(lst):
lst = [10, 20] # just rebinds the local label
numbers = [1, 2, 3]
try_replace(numbers)
print(numbers) # [1, 2, 3] — unchanged
Visualizing with id()
def show(obj):
print(id(obj)) # same address as the caller's object
x = [1, 2, 3]
print(id(x)) # e.g. 140234567
show(x) # same value — same object
How to Avoid Unintended Mutation
# Pass a copy
append_item(numbers[:]) # shallow copy
append_item(list(numbers)) # shallow copy
append_item(copy.deepcopy(numbers)) # deep copy
# Or make the function document its mutating intent clearly
Mutable Default Argument Trap
A classic Python gotcha:
# BAD — the list is created once and shared across calls
def add(item, lst=[]):
lst.append(item)
return lst
add(1) # [1]
add(2) # [1, 2] ← unexpected!
# GOOD
def add(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Key Interview Points
- Python is pass-by-object-reference: functions get a copy of the reference.
- Immutable objects (int, str, tuple) appear "pass-by-value" because you can't mutate them in place.
- Mutable objects (list, dict) can be mutated inside a function and the change is visible outside.
- Rebinding a local variable (
x = new_value) never affects the caller. - Never use a mutable object as a default argument — it is evaluated once at function definition.