"Least Astonishment" and the Mutable Default Argument

In Python, a default argument is an argument that assumes a default value if a value is not provided in the function call for that argument. Default arguments are defined in the function definition, and they are specified using the = operator.

For example, consider the following function definition:

def greet(name, greeting='Hello'):
  print(f'{greeting}, {name}!')

greet('John')
greet('Jack', 'Hi')

In this function definition, the greeting argument has a default value of 'Hello', so it is a default argument.

The principle of "least astonishment" suggests that the behavior of a function should be as predictable and unsurprising as possible. In the context of default arguments in Python, this principle suggests that the value of a default argument should not change between function calls.

Watch a course Python - The Practical Guide

However, it is possible to define a default argument as a mutable object, such as a list or a dictionary. If a default argument is defined as a mutable object and it is modified inside the function, the modification will persist across function calls, which can be surprising to some users.

For example, consider the following function definition:

def append_to_list(value, lst=[]):
  lst.append(value)
  return lst

print(append_to_list(1))
print(append_to_list(2))
print(append_to_list(3))

This function defines a default argument lst as an empty list. The function appends the value passed as an argument to the list and returns the modified list.

When we call this function with the values 1, 2, and 3, we might expect to get the following output:

[1]
[2]
[3]

However, the actual output is:

[1]
[1, 2]
[1, 2, 3]

This is because the default value of the lst argument is defined only once, when the function is defined. The same list object is used as the default value for all function calls, so modifications to the list persist across function calls.

To avoid this behavior and follow the principle of least astonishment, it is generally recommended to define default arguments as immutable objects, such as numbers, strings, or tuples. If you need to use a mutable object as a default argument, you can define the default value as None and then check for that value inside the function, like this:

def append_to_list(value, lst=None):
  if lst is None:
    lst = []
  lst.append(value)
  return lst

print(append_to_list(1))
print(append_to_list(2))
print(append_to_list(3))

This will produce the expected output:

[1]
[2]
[3]