šŸ¶
Python

Python Mutable Default Arguments: Avoid Surprises

By Filip on 10/05/2024

Learn how Python's mutable default arguments can lead to unexpected behavior (the "Principle of Least Astonishment") and how to avoid this common pitfall.

Python Mutable Default Arguments: Avoid Surprises

Table of Contents

Introduction

In Python, function arguments can have default values, which is a convenient feature. However, there's a subtle aspect to be aware of when using mutable objects like lists or dictionaries as default values. Let's explore this behavior and understand how to avoid potential pitfalls.

Step-by-Step Guide

In Python, you can define functions with default values for their arguments. This can be handy, but there's a catch when the default value is a mutable object like a list or a dictionary.

Let's say you have a function that appends a name to a list, and that list has a default empty list:

def add_name(name, names=[]):
  names.append(name)
  return names

You might expect this function to always start with an empty list and return a list with just the name you added. However, Python doesn't create a new empty list every time the function is called without the names argument. Instead, it uses the same list object that was created when the function was defined.

So, if you call add_name('Alice'), you'll get ['Alice'] as expected. But if you call it again with add_name('Bob'), you'll get ['Alice', 'Bob']. The list persists across function calls!

This behavior often surprises people and can lead to unexpected bugs. It happens because the default argument values are created only once when the function is defined, not every time it's called.

To avoid this issue, a common practice is to use None as the default value for mutable arguments:

def add_name(name, names=None):
  if names is None:
    names = []
  names.append(name)
  return names

Now, inside the function, you check if names is None. If it is, you create a new empty list. This ensures that each function call starts with a fresh list, preventing unexpected data sharing between calls.

Code Example

This Python code demonstrates the dangers of using mutable default arguments in functions. It shows how using a list as a default argument can lead to unexpected data sharing between function calls. The code provides two examples: one with the problematic behavior and another demonstrating the correct way to handle default arguments for mutable data types like lists.

# Example demonstrating the issue with mutable default arguments
def add_name_problematic(name, names=[]):
  names.append(name)
  return names

# Example demonstrating the solution with None as default
def add_name_safe(name, names=None):
  if names is None:
    names = []
  names.append(name)
  return names

# Using the problematic function
print("Using the problematic function:")
print(add_name_problematic('Alice'))  # Output: ['Alice']
print(add_name_problematic('Bob'))   # Output: ['Alice', 'Bob'] - Unexpected!

# Using the safe function
print("\nUsing the safe function:")
print(add_name_safe('Alice'))  # Output: ['Alice']
print(add_name_safe('Bob'))   # Output: ['Bob'] - As expected!

This code first defines the add_name_problematic function, which exhibits the unexpected behavior. We then call it twice, demonstrating how the list persists across calls.

Next, the code defines add_name_safe, which uses the None default and the check to create a new list when needed. We call this function twice as well, showing that it correctly starts with a fresh list each time, avoiding the unintended data sharing.

Additional Notes

  • This behavior stems from how Python treats default arguments ā€“ they are evaluated only once during the function definition, not every time the function is called.

  • Think of it like this: the default list is "attached" to the function itself. Each time you call the function without providing your own list, you're reusing the same "attached" list.

  • Why None? None acts as a signal within the function. It tells the function to create a brand new empty list specifically for that function call.

  • Key takeaway: Be cautious with mutable default arguments. If you need a fresh mutable object each time, use the None pattern to ensure your function behaves predictably.

  • Beyond lists: This issue applies to any mutable data type used as a default argument, including dictionaries and sets.

  • Debugging tip: If you encounter strange behavior where a function seems to "remember" data from previous calls, suspect mutable default arguments.

Summary

This article highlights a common pitfall in Python when using mutable objects (like lists or dictionaries) as default values for function arguments.

The Problem:

When a function is defined with a mutable object as a default argument, Python doesn't create a new object on each call. Instead, it reuses the same object from the function definition. This can lead to unexpected behavior, as modifications made within the function persist across calls.

Example:

def add_name(name, names=[]):  # Problem: default list is reused
  names.append(name)
  return names

Calling add_name('Alice') followed by add_name('Bob') results in ['Alice', 'Bob'], because the same list is modified on each call.

Solution:

To avoid this issue, use None as the default value for mutable arguments:

def add_name(name, names=None):  # Solution: create new list if None
  if names is None:
    names = []
  names.append(name)
  return names

This ensures a fresh list is created within the function if no argument is provided, preventing unintended data sharing between calls.

Conclusion

Understanding how Python handles default arguments, especially with mutable objects, is crucial for writing predictable and bug-free code. Always remember that mutable default arguments are evaluated only once during function definition. If you need to start fresh with a mutable object on each function call, use the None check pattern to ensure your functions behave as expected. This practice will save you from unexpected results and potential debugging headaches down the road.

References

Were You Able to Follow the Instructions?

šŸ˜Love it!
šŸ˜ŠYes
šŸ˜Meh-gical
šŸ˜žNo
šŸ¤®Clickbait