A default argument in Python is evaluated once, when the function is defined. If that default is a list or a dict, every call shares the same object.
This is the single most reported surprise in Python, and it produces bugs that look like the function is remembering old calls.
The bug
Three calls, each meant to start with an empty basket.
# the bug
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item('apple'))
print(add_item('bread'))
print(add_item('milk'))
It prints:
['apple']
['apple', 'bread']
['apple', 'bread', 'milk']
The basket keeps growing. Nobody passed it in, and it was never emptied.
Why it happens
The default is stored on the function object itself. You can look at it directly.
# why it happens
def add_item2(item, basket=[]):
basket.append(item)
return basket
print(add_item2.__defaults__)
add_item2('x')
print(add_item2.__defaults__)
It prints:
([],)
(['x'],)
__defaults__ holds the same list the function hands out. Appending inside the body appends to that stored list, so the next call sees it.
The fix
Use None as the default and build the real value inside the function.
# the fix
def add_item3(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item3('apple'))
print(add_item3('bread'))
It prints:
['apple']
['bread']
Now the list is created per call, because the body runs per call.
It applies to every mutable default
Lists, dicts and sets all behave this way. Immutable defaults such as numbers, strings, tuples and dates are safe, because nothing can change them.
# it applies to every mutable default
import datetime
def log(message, seen={}, stamps=[], when=datetime.date(2020, 1, 1)):
seen[message] = True
stamps.append(when)
return len(seen), len(stamps)
print(log('first'))
print(log('second'))
It prints:
(1, 1)
(2, 2)
The date default is fine. The dict and the list are not.
The same trap in a class body
A list assigned in the class body belongs to the class, so every instance shares it.
# the same trap in a class body
class Basket:
items = [] # shared by every instance
a, b = Basket(), Basket()
a.items.append('apple')
print(b.items, a.items is b.items)
class BasketFixed:
def __init__(self):
self.items = []
c, d = BasketFixed(), BasketFixed()
c.items.append('apple')
print(d.items, c.items is d.items)
It prints:
['apple'] True
[] False
Assign mutable attributes in __init__, not in the class body.
What to remember
- Default arguments are evaluated once, at definition time.
- A mutable default is shared by every call that does not pass one.
- Use
Noneas the default and create the value inside the function. - The same rule applies to mutable attributes in a class body.
Dataclasses raise a ValueError if you try this, which is one of the few places Python stops you rather than letting it happen quietly.