How to pass a variable by reference in Python
Python has neither pass-by-value nor pass-by-reference, and the usual explanation — “mutable types are passed by reference, immutable ones by value” — is close enough to sound right and wrong enough to mislead. This article takes it apart.
The observation everyone starts from
def try_rebind(x):
x = 99
def try_mutate(x):
x.append(99)
int after rebinding inside a function : 1 <- unchanged
list after .append inside a function : [1, 99] <- changed
From which it looks as though lists and ints are passed differently. They are not.
The same list, both ways
def rebind_list(x):
x = [99]
def mutate_list(x):
x.append(99)
after rebind_list(a) : [1] <- unchanged
after mutate_list(a) : [1, 99] <- changed
Same type, same signature, different outcome. So the distinction is not list-versus-int. It is rebinding versus mutating.
What actually happens
Nothing is copied. The parameter becomes a second name for the same object:
id(obj) outside : 140234...
id of the parameter : 140234...
same object? : True
Assignment inside the function binds that local name to something else. The caller’s name is untouched, because it was never the same name — only the object was shared:
id inside : 140234...
id after rebinding inside : 140891... <- a different object
outside is still : 'original'
This is sometimes called call by assignment: passing an argument works exactly like an assignment statement, and assignment in Python binds names to objects rather than copying values into boxes.
Once you see it that way, the mutable/immutable business falls out as a consequence rather than a rule. Immutable objects cannot be changed in place, so mutating them is not an option:
tuple[0] = 9 -> TypeError: 'tuple' object does not support item assignment
str[0] = 'z' -> TypeError: 'str' object does not support item assignment
Only rebinding is available, and rebinding never reaches the caller. Hence the
appearance of pass-by-value for int, str and tuple.
Where += stops making sense
def plus_equals_list(x):
x += [99]
def plus_equals_int(x):
x += 1
list after x += [99] inside : [1, 99] <- CHANGED
int after x += 1 inside : 1 <- unchanged
Identical syntax, opposite results. += tries
__iadd__
first, which mutates in place and returns the same object. list has one, so
x += [99] is x.extend([99]) and the caller sees it. int does not, so
Python falls back to x = x + 1, which is a rebinding.
So += is a mutation or a rebinding depending on the type on the left. That is
worth knowing before you use it on a parameter.
The trap that does both
The best demonstration of the above, and a genuinely odd one:
t = ([1], 2)
t[0] += [9]
t[0] += [9] on ([1], 2) -> TypeError: 'tuple' object does not support item assignment
...and yet t is now ([1, 9], 2) <- the mutation HAPPENED before the error
It raised and it worked. += on the inner list succeeded via __iadd__,
mutating it in place. Then Python tried to store the result back into t[0],
which a tuple refuses — but the list had already changed.
If you ever wanted proof that += is two operations rather than one, this is
it.
How to get a value back out
There is no & or ref in Python. Three options, in the order I would reach
for them.
Return it. Almost always the right answer:
b = returns_it(b)
return a new value and rebind : [1, 99]
Mutate a container you were given. Fine when the function’s job is to fill something in, though it makes the function harder to reason about.
Wrap it in an object:
class Box:
def __init__(self, value): self.value = value
def set_box(box):
box.value = 99
wrap it in an object : 99
Attribute assignment is a mutation of the object, not a rebinding of the name, so it reaches the caller.
The consequence you will meet first
Default arguments are evaluated once, at definition time, so a mutable default is shared by every call:
def collect(item, into=[]):
into.append(item)
return into
first call : ['a']
second call : ['a', 'b'] <- the SAME list
The fix is the standard one:
def collect_ok(item, into=None):
if into is None:
into = []
with None : ['a'] then ['b']
When you do not want to share
list(orig)[0] is orig[0] : True
deepcopy(orig)[0] is orig[0] : False
list(x), x[:] and x.copy() all copy one level. If the function might
mutate something nested, only
copy.deepcopy
protects the caller.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).