"Least Astonishment" and the Mutable Default Argument

Python is a powerful and flexible programming language, but it can also have some unexpected behavior that can be confusing, especially for newcomers. One such issue is the behavior of mutable default arguments in function definitions.

Understanding the Problem

In Python, when a function is defined with default arguments, the values of those arguments are bound to the function object at the time of function definition, not at the time of function execution. This means that if the default argument is mutable, such as a list or dictionary, changes made to the argument will persist across multiple function calls.

For example, consider the following function:

def foo(a=[]):
    a.append(5)
    return a

One might expect that calling this function multiple times without passing any arguments would always return a list with only one element: [5]. However, the actual behavior is very different:

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

This behavior can be surprising and unexpected for those new to Python, and is often referred to as the "mutable default argument problem."

The Reason for the Design

So why does Python bind the default argument at function definition instead of at function execution? The reason lies in the scoping rules of Python and the desire to have consistent behavior.

When a function is defined, its default arguments become part of the function's local scope. This means that they are evaluated and bound to their default values at the time of function definition, not at the time of function execution.

If Python were to bind default arguments at function execution, it would introduce ambiguity and potential bugs. Consider the following example:

def a():
    print("a executed")
    return []

           
def b(x=a()):
    x.append(5)
    print(x)

In this example, the function a is called to provide the default value for the argument x in function b. If the default argument binding occurred at function execution, the function a would be called every time b is called, resulting in the string "a executed" being printed every time.

By binding default arguments at function definition, Python ensures that the default values are evaluated and assigned only once, resulting in consistent behavior.

Working Around the Issue

While the default argument behavior can be surprising, there are ways to work around it to achieve the desired results.

One common approach is to use None as the default value and explicitly check for it within the function:

def foo(a=None):
    if a is None:
        a = []
    a.append(5)
    return a

By doing this, we ensure that a new list is created each time the function is called without an argument, achieving the expected behavior of always returning a list with only one element.

Conclusion

While the mutable default argument problem in Python can be initially puzzling, understanding why it occurs and how to work around it can help developers avoid unexpected behavior and write more robust code.