Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Mutable default arguments PREMIUM

Series: Functions
Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read Watch as video Python 3.9—3.13
Python Morsels
Watch as video
03:47

Let's talk about the problem with mutable default argument values in Python.

Functions can have default values

Function arguments in Python can have default values. For example this greet function's name argument has a default value:

>>> def greet(name="World"):
...     print(f"Hello, {name}!")
...

When we call this function without any arguments, the default value will be used:

>>> greet()
Hello, World!
>>>

Default values are great, but they have one gotcha that Python developers sometimes overlook.

A shared default value

Let's use a default value to pick a random color from a list of colors by using random.choice:

>>> import random
>>> def pick_color(color=random.choice(['red', 'blue', 'green'])):
...     return color

When we call this function without any arguments, we'll get a random color:

>>> pick_color()
'green'

But if we call it again without arguments, we'll get the same color:

>>> pick_color()
'green'

That's not a coincidence. Anytime we call this function without arguments, we'll always get the same color.

>>> pick_color()
'green'
>>> pick_color()
'green'
>>> pick_color()
'green'

Why?

Well, default argument values are only evaluated one time: when the function is defined.

Default values are only evaluated once

Default values are only evaluated when a function is defined.

import random

def pick_color(color=random.choice(['red', 'blue', 'green'])):
    return color

In fact, we can even see the default argument values that are attached to a function by looking at certain dunder attributes on that function object:

>>> pick_color.__defaults__
('green',)

Default values are evaluated when a function is defined, not when it's called.

So we should be careful when defining our default argument value, that we don't call functions that could return different values depending on when they're called.

Mutable default arguments can be trouble

Using a mutable object for a default function argument can also be a problem.

For example, here we have a class called TodoList:

class TodoList:
    def __init__(self, tasks=[]):
        self.tasks = tasks

    def add_task(self, task):
        self.tasks.append(task)

This class's __init__ method will have just one value for our tasks list that will be shared between all calls to this method.

So if we make two instances of this class:

>>> mon = TodoList()
>>> tue = TodoList()

And then we mutate the value of the tasks list on one of these two instances:

>>> mon.add_task("Work on Python exercise")

We'll see that the tasks list changes on both of the instances!

>>> mon.tasks
['Work on Python exercise']
>>> tue.tasks
['Work on Python exercise']

Shared argument values are the real problem

But default arguments aren't the only argument-related issue in Python.

For example, if we pass the same list to two different TodoList objects:

>>> initial_tasks = ['Watch Python screencast', 'brush teeth']
>>> mon = TodoList(initial_tasks)
>>> tue = TodoList(initial_tasks)

And we modify the tasks list on one of those objects:

>>> mon.add_task("Work on Python exercise")

We'll see that it will also modify on the other object:

>>> mon.tasks
['Watch Python screencast', 'brush teeth', 'Work on Python exercise']
>>> tue.tasks
['Watch Python screencast', 'brush teeth', 'Work on Python exercise']

The problem is that we're taking whatever list was passed into our class and we're storing it directly on our class instance.

Avoiding shared argument issues by copying

We could avoid this issue by using the list copy method:

class TodoList:
    def __init__(self, tasks=[]):
        self.tasks = tasks.copy()

    def add_task(self, task):
        self.tasks.append(task)

But if we use the built-in list function instead, we could make a copy of our list and accept any iterable, not just lists:

class TodoList:
    def __init__(self, tasks=[]):
        self.tasks = list(tasks)

    def add_task(self, task):
        self.tasks.append(task)

Because the list function will accept any iterable in Python, whereas the copy method only exists on list objects.

Note that this also happens to resolve our problem with default argument values. Two TodoList objects without any initial tasks will always end up with separate tasks lists:

>>> mon = TodoList()
>>> tue = TodoList()
>>> mon.add_task("Work on Python exercise")
>>> mon.tasks
['Work on Python exercise']
>>> tue.tasks
[]

So one way to avoid issues with mutable default arguments is to copy the argument that's coming into our function. This is something we should especially consider for class initializers.

Avoiding mutable default values entirely

Another very common way to avoid issues with default argument values is to avoid using mutable default values. For example, it's pretty common to see None used as a default value.

Here, whenever our class initializer doesn't have a tasks iterable specified, it'll use an empty list:

class TodoList:
    def __init__(self, tasks=None):
        if tasks is None:
            tasks = []
        else:
            self.tasks = list(tasks)

    def add_task(self, task):
        self.tasks.append(task)

In our case, though, there's an even simpler way we could have avoided mutable default values. We could have used a tuple:

class TodoList:
    def __init__(self, tasks=()):
        self.tasks = list(tasks)

    def add_task(self, task):
        self.tasks.append(task)

Lists are mutable sequences, and tuples are immutable sequences, and both are iterables, so the list function will accept either one just fine.

Be careful with Python's default argument values

In Python, default argument values are defined when a function is defined. So every time you call a function, it'll use the same default argument value.

You should be very cautious when using default argument values that aren't simple immutable objects.

Series: Functions

Python, like many programming languages, has functions. A function is a block of code you can call to run that code.

Python's functions have a lot of "wait I didn't know that" features. Functions can define default argument values, functions can be called with keyword arguments, and functions can be written to accept any number of arguments.

To track your progress on this Python Morsels topic trail, sign in or sign up.

0%
Python Morsels
Watch as video
03:47
This is a free preview of a premium screencast. You have 2 previews remaining.