Execute Program

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 = 3

    def f(x=default_value):
    return x

    default_value = 4
    f()
    Result:
    3Pass Icon
  • 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_list

    list_1 = add_one()
    list_2 = add_one()
    list_2
    Result:
  • >
    def add_one(my_list=[]):
    my_list.append(1)
    return my_list

    list_1 = add_one()
    list_2 = add_one()
    list_3 = add_one()
    list_3
    Result:
    [1, 1, 1]Pass Icon
  • 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_list

    list_1 = add_one()
    list_2 = add_one()
    list_1 is list_2
    Result:
    TruePass Icon
  • 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_name
    return 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'}Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    rename_user(new_name="Betty")
    Result:
    {'name': 'Betty'}Pass Icon
  • 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'}Pass Icon
  • 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 None as 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. None is 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_2
    Result:
    [1]Pass Icon
  • 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_2
    Result:
    FalsePass Icon
  • Here's a code problem:

    This new_patient function takes an optional allergies argument, 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_patient so 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:
    NonePass Icon
  • 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.