Python for Programmers: Mutable Default Arguments
Welcome to the Mutable Default Arguments lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In an earlier lesson, we saw that default argument values are evaluated when the function is defined, not when it's called.
>
default_value = 3def f(x=default_value):return xdefault_value = 4f()Result:
3
This language quirk is particularly tricky with mutable values. (Mutable values, like lists or dictionaries, can change over time. For example, we can append a new value to a list, or add a new key to a dictionary. Immutable values, like integers or strings, never change.)
Suppose that our default value is an empty list. Because the default value is only evaluated when the function is defined, only one list is ever created. Every call to the function might change the list's contents. But the same list is used in every call, so changes to the list during one function call are still visible during future calls!
>
def add_one(my_list=[]):my_list.append(1)return my_listlist_1 = add_one()list_2 = add_one()list_2Result:
>
def add_one(my_list=[]):my_list.append(1)return my_listlist_1 = add_one()list_2 = add_one()list_3 = add_one()list_3Result:
[1, 1, 1]
This happens because they're the same list. Not just lists with the same contents, but exactly the same list at the same location in memory!
>
def add_one(my_list=[]):my_list.append(1)return my_listlist_1 = add_one()list_2 = add_one()list_1 is list_2Result:
True
Here's a different example where we use a dictionary as the default value. It seems to work normally when we call it.
>
def rename_user(user={"name": "Amir"}, new_name=None):if new_name is not None:user["name"] = new_namereturn user- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
rename_user()Result:
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
rename_user(new_name="Amir")Result:
{'name': 'Amir'} - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
rename_user(new_name="Betty")Result:
{'name': 'Betty'} After calling
rename_user("Betty"), the dictionary used as our default value has changed. It's now{"name": "Betty"}, not{"name": "Amir"}.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
# Remember that the default user dict in our function is `{"name": "Amir"}`.rename_user(new_name="Betty")rename_user()Result:
{'name': 'Betty'} This violates our mental model of how functions work. If a function changes a global variable, or works with files, then we might expect one function call to affect the result of future calls. The functions in this lesson aren't doing anything like that; they're only using a default argument value. But because the default value is mutable, calling
rename_user()gives different results depending on the history of how we've called the function in the past.Using mutable values like lists as default arguments is almost always a bad idea. One workaround is to use
Noneas a "sentinel value" instead.(Sentinels are a general pattern in programming, not specific to Python. They're a kind of "placeholder" value: we use them to clearly mark that something is missing, or that there's not a normal value here.
Noneis a common sentinel value, but we can use other values too.)We can use a sentinel value to fix the previous example's function. Inside the function, we check for whether the argument is
None, then create the default value we actually want: a distinct empty list. The critical difference is that this list is a regular local variable in the function, so different function calls can't affect each other.>
def add_one(my_list=None):if my_list is None:my_list = []my_list.append(1)return my_list- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
list_1 = add_one()list_2 = add_one()list_2Result:
[1]
Now we create a new list each time, which we can verify with
is.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
list_1 = add_one()list_2 = add_one()list_1 is list_2Result:
False
Here's a code problem:
This
new_patientfunction takes an optionalallergiesargument, which defaults to an empty list. It seems straightforward, but try running the code to see the failing assertion. Something is wrong: Betty's allergies to strawberries and eggs end up in Amir's allergy list.Debug the problem, fixing
new_patientso that the assertions all pass.def new_patient(name, allergies=None):if allergies is None:allergies = []return {"name": name,"allergies": allergies,}def add_allergy(person, allergy):person["allergies"].append(allergy)amir = new_patient("Amir")add_allergy(amir, "dogs")betty = new_patient("Betty")add_allergy(betty, "strawberries")add_allergy(betty, "eggs")cindy = new_patient("Cindy", ["cats"])assert amir["allergies"] == ["dogs"]assert betty["allergies"] == ["strawberries", "eggs"]assert cindy["allergies"] == ["cats"]- Goal:
None
- Yours:
None
One final question: how big a problem is this in practice? Fortunately, we have a famous outage as an answer!
Digg version 4's launch in 2010 went very poorly. After they stabilized the initial launch, the servers still required manual restarts every four hours.
Those restarts were ultimately required because of a default argument value. A programmer at Digg thought that a list used as a default argument value would be re-initialized on each function call. As we saw in this lesson, every call actually gets the same list. In Digg's case, the list continued to grow after each function call, consuming more and more resources. "It took a month to track this bug down," at which point they could finally stop manually restarting the servers.