Show the mutable default argument bug in a function, and the standard fix.
Default argument values are worked out once, when the def statement runs, and not on each call. That means a mutable default is created a single time and then reused by every call that leaves the argument out, so any change to it is visible to the next caller.
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item('a')) # ['a']
print(add_item('b')) # ['a', 'b'] <- the old state is carried over
print(add_item('c')) # ['a', 'b', 'c']
def add_item_fixed(item, bucket=None):
if bucket is None:
bucket = [] # a fresh list on every call
bucket.append(item)
return bucket
print(add_item_fixed('a')) # ['a']
print(add_item_fixed('b')) # ['b']
The first three calls share one list, so the output grows every time. The fixed version creates a new list on each call, so every result contains only its own item.
The fix has one subtlety. The check must be written as if bucket is None and not as if not bucket. If the caller passes an empty list on purpose, a test for "not bucket" would throw that list away and replace it with a new one, which is a different bug in the opposite direction.
The reason the default is only worked out once is that the def statement itself runs only once, when the function is defined. The default expression is evaluated at that moment, and the resulting object is stored in the definition of the function and then handed to every call that does not supply the argument. That is exactly why immutable defaults such as 0, an empty string, or a tuple are safe, and why only mutable defaults are dangerous.