What are mutable and immutable objects in Python, and why is a mutable default argument a bug?
Mutable objects such as lists, dicts and sets can change in place; immutable ones such as int, str and tuple cannot. Default arguments are evaluated once, when the function is defined, so a list used as a default is shared across every call. Use None as the default and create the list inside the function.
Explain it simply
A mutable object is like a whiteboard in a classroom. Anyone with access can wipe and rewrite it, and everyone sees the change. An immutable object is like a printed notice. You cannot change it; you can only print a new notice and pin that up instead.
In Python, every variable is a name that points to an object. With an immutable object, "changing" the variable really makes a new object and moves the name. With a mutable object, you can change the object itself, and every name pointing to it sees the new content.
- Immutable:
int,float,bool,str,tuple,frozenset,bytes. - Mutable:
list,dict,set,bytearray, and most objects of classes you write.
Example
def add_student(name, batch=[]): # bug: one list shared by all calls
batch.append(name)
return batch
print(add_student("Asha")) # ['Asha']
print(add_student("Ravi")) # ['Asha', 'Ravi'] <- Asha is still there!
def add_student_fixed(name, batch=None):
if batch is None:
batch = [] # a fresh list on every call
batch.append(name)
return batch
print(add_student_fixed("Asha")) # ['Asha']
print(add_student_fixed("Ravi")) # ['Ravi']How it works
When Python runs the def line, it evaluates the default values once and stores them on the function object (you can see them in add_student.__defaults__). Every call that does not pass batch gets that same stored list. Because a list is mutable, append changes the stored list itself, so the next call starts with the leftovers.
The same thing does not happen with count=0, because count += 1 on an int creates a new int and rebinds the local name. The stored default 0 never changes.
You can see identity with id() and is:
a = [1, 2]
b = a # same list, two names
b.append(3)
print(a) # [1, 2, 3]
print(a is b) # TrueReal-world use
This bug shows up in real code: a Flask helper that collects error messages in a default list ends up showing one user's errors to the next user. The same idea explains why passing a dict into a function and changing it inside changes it for the caller too. Knowing which types are mutable also tells you which ones can be dictionary keys.
Common mistakes
- Using
[],{}orset()as a default argument. - Thinking
b = acopies a list. It only creates a second name for the same list. - Assuming that because strings are immutable,
s += "x"is an error. It works; it just builds a new string. - Checking for the default with
if not batch:instead ofif batch is None:. An empty list passed on purpose would then be replaced.
What to say in the interview
"Mutable objects like lists, dicts and sets can be changed in place, while immutable ones like ints, strings and tuples cannot; any change creates a new object. Default arguments are evaluated once when the function is defined, not on each call. So if the default is a list, every call shares the same list, and items pile up across calls. The standard fix is to use None as the default and create a new list inside the function."
Follow-up questions
- Why does
count=0as a default not have the same problem? - What is the difference between
==andis? - Are strings passed by value or by reference in Python?
- Is there any case where a mutable default is useful on purpose?
Go deeper
Read these once the basics above feel easy.
Python passes arguments by "object reference" (sometimes called pass by assignment). The function gets a new name bound to the same object. If the object is mutable and the function changes it, the caller sees the change. If the function rebinds the name (batch = []), the caller is unaffected.
A mutable default is occasionally used on purpose as a simple cache that survives between calls, but functools.lru_cache is a clearer way to do that.