Skip to content
Theory · Lists · lesson 8 of 11

A mutable default

Picture a café that hands its own mug to a customer who did not bring one. It is convenient: you need to bring nothing. But the café has only one mug for everyone, and nobody washes it. The second customer gets it with the rest of someone else's coffee.

A default argument works in a similar way. It saves a call from passing an extra argument. But if the default value is a list, the function starts remembering past calls:

Python
def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags


print(add_tag("red"))  # ['red']
print(add_tag("blue"))  # ['red', 'blue']
print(add_tag("new", []))  # ['new']

Let's go through the three calls:

  1. add_tag("red") does not pass tags and gets the shared list. It is empty, 'red' goes into it, and the answer is right.
  2. add_tag("blue") gets the same shared list, which already holds 'red'. The answer is ['red', 'blue'], even though the second call knows nothing about the first.
  3. add_tag("new", []) passes its own empty list and gets the right answer, ['new'].

Interviewers like to ask this: "What does this program print, and why?" To answer, you need to know when Python creates a default value.

When the value is created

A default value is evaluated once—when the def line runs, not on every call. Python stores the resulting object with the function, and every call without a tags argument gets that same object. That is the café's mug: it was handed out once, when the café opened.

For a number or a string this goes unnoticed: they cannot be changed. A list can be changed, so append adds to one shared list that lives on between calls. The first call gets it empty and answers correctly, so the error shows only from the second call on.

The third call passed its own list, and the parameter tags became a name for the new []. That call did not touch the shared list: it still holds ['red', 'blue'].

This is where the café analogy ends: in a café you can at least see the dirty mug. In Python the shared list is hidden inside the function, and from the outside the calls look independent.

The None value

To give each call a new list, the default value is None, and the list itself is created in the function body. In the café, that means a guest without a mug gets a new, clean one every time.

Python
def add_tag(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags


print(add_tag("red"))  # ['red']
print(add_tag("blue"))  # ['blue']

Let's go through the function body:

  1. if tags is None: checks whether the argument was passed.
  2. tags = [] runs on every call without tags and creates a new list each time.
  3. From then on tags always refers to a list: either the one passed or a newly created one.

None serves here as a sentinel—a sign that the argument was not passed. The is None check tells such a call apart from a call with an empty list. The same is done for any mutable default value, not only for a list.

Common mistake

The is None check was replaced with or:

Python
def add_tag(tag, tags=None):
    tags = tags or []  # ✗
    tags.append(tag)
    return tags


mine = []
add_tag("red", mine)
print(mine)  # []

The or operator checks truthiness, not whether the argument was passed. The empty list mine is falsy, so tags or [] gives a new list: "red" goes into it, and mine stays empty. Such a function works correctly until the calling code passes an empty list. With the if tags is None: check, the function would have added "red" to mine.

Key points

  • A default value is evaluated once, when def runs, and is shared by all calls.
  • A list as a default value collects data between calls.
  • The default value is None, and a new list is created in the function body after an is None check.
  • tags or [] also replaces an empty list that was passed, so an is None check is needed.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way