If you’ve ever seen a list or dictionary mysteriously change between function calls or across different instances of a class, you’re not alone. This behavior usually stems from using mutable default arguments, a classic Python gotcha. Let’s look at what causes it and how to avoid it.
Description of the Problem
You might encounter a strange situation where a list (or dictionary) you define as a default argument in a function or a class is changing unexpectedly across calls or object instances.
For example:
def add_item(item, collection=[]): collection.append(item) return collection
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] ← Unexpected?
Or in a class:
class Cart: def __init__(self, items=[]): self.items = items
cart1 = Cart()
cart1.items.append("apple")
cart2 = Cart()
print(cart2.items) # ['apple'] ← Wait, what?
Why This Happens
This behavior is due to a common Python pitfall involving mutable default arguments. In Python, default argument values are evaluated only once at the time the function or class is defined, not each time it’s called.
So when you define collection=[], that same list object is reused in every function call or class instance unless explicitly overridden. This leads to shared state and subtle bugs.
Steps to Resolve the Issue
Step 1: Use None as the Default Argument and Create Inside the Function
This is the standard workaround in Python:
def add_item(item, collection=None): if collection is None: collection = [] collection.append(item) return collection
Each call now gets its own list, avoiding shared state:
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana']
Step 2: Apply the Same Principle in Class Constructors
Fixing the class example:
class Cart: def __init__(self, items=None): if items is None: items = [] self.items = items
Now each instance of Cart has its own independent list.
Step 3: Be Careful with All Mutable Defaults
This issue also applies to dictionaries, sets, or custom objects.
Bad:
def config(settings={}): # Shared across calls!
Good:
def config(settings=None): if settings is None: settings = {}
Quick Recap
In Python, default mutable arguments (like lists or dicts) are shared across calls because they are evaluated only once at definition time. This can lead to bugs that are hard to track, especially in larger codebases or shared modules.
Best Practices:
- Never use mutable objects as default arguments
- Use None and create a new object inside the function or constructor
- Review any third-party code or libraries that might use mutable defaults, it’s a common gotcha even among experienced developers
Conclusion
Using mutable default arguments in Python can cause unexpected behavior, especially in larger applications where function calls and class instances stack up. To avoid these hard-to-spot bugs, it’s best to follow safe patterns or better, hire Python developers who already know how to structure code defensively and cleanly.