Python Pitfalls: The Perils of Using Lists and Dicts as Default Arguments

Python Pitfalls: The Perils of Using Lists and Dicts as Default Arguments


images/python-pitfalls--the-perils-of-using-lists-and-dicts-as-default-arguments.webp

Python, renowned for its simplicity and readability, occasionally hides subtle complexities that can lead to unexpected behaviors, especially for novice programmers. One such quirk involves using mutable types like lists and dictionaries as default arguments in function definitions.

Understanding Mutable Default Arguments

Consider a seemingly innocent function:

def add_item(name, item_list=[]):
    item_list.append(name)
    return item_list

At first glance, this function adds a name to a list and returns it. However, the problem lies with using a list (a mutable type) as a default argument. Python’s default arguments are evaluated only once when the function is defined, not each time the function is called. This means that the item_list is shared across all calls to add_item that don’t provide an item list.

Demonstrating the Issue

Let’s see what happens in practice:

first_call = add_item('apple')
second_call = add_item('banana')

print("First call:", first_call)
print("Second call:", second_call)

You might expect two separate lists, but the output is surprising:

First call: ['apple']
Second call: ['apple', 'banana']

Both calls return the same list, modified by both function calls. This shared state can lead to confusing bugs, especially in larger, more complex codebases.

The Safe Alternative: None

A better approach is to use None as a default argument and then check for it within the function:

def add_item(name, item_list=None):
    item_list = item_list or []
    item_list.append(name)
    return item_list

Now, each call to add_item without a specific list creates a new list:

first_call = add_item('apple')
second_call = add_item('banana')

print("First call:", first_call)
print("Second call:", second_call)

This code produces the expected result:

First call: ['apple']
Second call: ['banana']

The Case with Dictionaries

The same principle applies to dictionaries, another mutable type. Consider a function that maintains a record of scores:

def update_score(name, score, record={}):
    record[name] = score
    return record

Again, the record dictionary is shared across all calls, leading to unexpected results. The remedy is the same: use None as the default argument and assign a new dictionary within the function.

Best Practices and Conclusion

In summary, while Python’s default argument feature is handy, it requires careful consideration when dealing with mutable types. Remember:

  1. Use Immutable Defaults: Stick to immutable types like integers, strings, or None for default arguments.
  2. Create New Instances Inside: For lists or dictionaries, initialize them within the function body if the argument is None.
  3. Understand Python’s Behavior: Realize that default arguments are evaluated once at the time of function definition, not each call.

By adhering to these practices, you can avoid a common pitfall that often stumps even experienced Python developers. This understanding not only prevents bugs but also leads to clearer, more predictable code.


About PullRequest

HackerOne PullRequest is a platform for code review, built for teams of all sizes. We have a network of expert engineers enhanced by AI, to help you ship secure code, faster.

Learn more about PullRequest

PullRequest headshot
by PullRequest

December 20, 2023