Blog Post #90: The Mutable Default Argument Pitfall: A Common Beginner Mistake

This post covers what is arguably one of the most famous and confusing “gotchas” in the Python language. In Post #89, we learned how to use default parameter values, which is a fantastic feature for making functions more flexible. However, using a mutable type (like a list or a dictionary) as a default can lead to completely unexpected and buggy behavior.

We will demonstrate this pitfall, explain exactly why it happens, and show you the correct, professional pattern to avoid it.

The Unexpected Behavior

Let’s write a seemingly innocent function. It’s supposed to add an item to a list. If we don’t provide a list, it should create a new list for us, add the item, and return it.

def add_to_list(item, my_list=[]):
    my_list.append(item)
    return my_list

Now, let’s call it a few times without providing the my_list argument. We would expect each call to return a new list containing just one item.

# First call
list1 = add_to_list(1)
print(f"List 1: {list1}")

# Second call
list2 = add_to_list(2)
print(f"List 2: {list2}")

# Third call
list3 = add_to_list(3)
print(f"List 3: {list3}")

The output is baffling and wrong:

List 1: [1]
List 2: [1, 2]
List 3: [1, 2, 3]

Instead of creating a new list each time, the function seems to be remembering the items from the previous calls!

Why Does This Happen? The Single Object Rule

This strange behavior happens because of a core rule in Python:

Default parameter values are created only ONCE, at the moment the function is defined, not each time the function is called.

When Python first reads def add_to_list(item, my_list=[]):, it creates a single, empty list object in memory. The my_list parameter gets a reference to this one specific list as its default.

Every subsequent call to Notes that doesn’t provide its own list argument will use and modify that exact same list object. The first call appends 1 to the shared list. The second call appends 2 to that same list, which already contains 1.

The Correct Pattern: Use None as the Default

The standard and “Pythonic” way to safely have a mutable default is to use None as the default value, and then create a new list inside the function if one isn’t provided. None is immutable, so it is completely safe to use as a default.

The pattern is:

  1. Set the default value for the parameter to None.
  2. Inside the function, check if the parameter is None.
  3. If it is, create a new, empty mutable object (like a list).

Here is the corrected version of our function:

def add_to_list_safe(item, my_list=None):
    if my_list is None:
        my_list = [] # Create a new list only for this function call
    my_list.append(item)
    return my_list

This pattern ensures that a fresh, new list is created for every single function call that relies on the default.

The Correct Behavior in Action

Now, let’s run our test again with the safe version of the function.

# First call
list1 = add_to_list_safe(1)
print(f"List 1 (safe): {list1}")

# Second call
list2 = add_to_list_safe(2)
print(f"List 2 (safe): {list2}")

# Third call
list3 = add_to_list_safe(3)
print(f"List 3 (safe): {list3}")

The output is now exactly what we originally expected:

List 1 (safe): [1]
List 2 (safe): [2]
List 3 (safe): [3]

What’s Next?

The mutable default argument pitfall is a classic Python rite of passage. Remember the rule: Never use a mutable type (like a list or dictionary) as a default parameter value. Instead, use None as a default and create the mutable object inside the function. This will save you from some very confusing bugs.

Our functions have been doing work and printing things, but they haven’t been giving data back to the part of the code that called them in a structured way. To do that, we need a special keyword. In Post #91, we will explore the return statement.

Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com

Leave a Comment