Avoid one of Python’s most subtle and dangerous gotchas — the mutable default argument trap.
If you’ve been writing Python for a while, you might’ve stumbled upon strange behavior like this:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("apple"))
print(add_item("banana"))
print(add_item("cherry"))You probably expect:
["apple"]
["banana"]
["cherry"]But instead, you get:
['apple']
['apple', 'banana']
['apple', 'banana', 'cherry']😳 Wait, what? Why does the list keep growing when the function should start fresh every time?
Welcome to one of Python’s most infamous pitfalls: mutable default arguments.
In Python, default parameter values are evaluated only once — at function definition time, not every time the function is called.
That means when you write:
def add_item(item, items=[]):Python creates one list in memory when this line runs, and every future call uses the same list object.
You can confirm this with id():
def add_item(item, items=[]):
print(id(items))
items.append(item)
return items
add_item("apple")
add_item("banana")You’ll see the same memory address printed twice — meaning it’s literally the same list.
-
The function behaves unpredictably — data “leaks” between calls.
-
Bugs are hard to detect, especially in large codebases.
-
This affects all mutable types: list, dict, set, custom objects, etc.
Use None as the default value and initialize inside the function:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsNow it behaves as expected:
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana']Each call gets a fresh list — safe and predictable.
| Type | Safe Default | Example |
|---|---|---|
Mutable (list, dict, set) |
None |
def f(x=None): x = x or [] |
Immutable (int, str, tuple, bool) |
Direct value | def f(x=0): ... |
So this is perfectly fine:
def greet(name="Guest"):
return f"Hello, {name}!"…but this is not:
def add_item(item, items=[]): # ❌
...Let’s take a practical example from a system:
def has_access(base_id=[], id_checking_list=[]):
return not base_id or base_id in id_checking_listHere, base_id and id_checking_list are shared lists across all calls.
This can cause unexpected access mismatches between users.
def has_access(base_id=None, id_checking_list=None):
id_checking_list = id_checking_list or []
return not base_id or base_id in id_checking_listNow each call is independent, safe, and bug-free.
When you define a function:
def f(x=[]): ...Python internally does something like this:
_x_default = []
def f(x=_x_default): ...That _x_default is stored in memory for the lifetime of the program — it’s reused across all calls.
That’s why mutating it once mutates it for all.
Even if you think your function won’t modify the list, using None is a clean, future-proof habit.
def process_data(data=None):
data = data or []| Concept | Wrong | Right |
|---|---|---|
| Mutable default | def f(x=[]) |
def f(x=None) |
| Evaluation time | Once (on define) | Every call (safe) |
| Side effects | Shared state | Independent state |
| Applies to | list, dict, set, custom mutable objects | same rule applies |
Mutable default arguments are one of Python’s most common and subtle bugs — even experienced developers have been burned by them.
So next time you’re defining a function, remember this one simple rule:
“Never use mutable objects as default arguments — use
Noneinstead.”
It’ll save you hours of debugging and keep your functions pure, predictable, and professional.
If you’re working in a team, consider adding this rule to your flake8 or pylint configuration:
B006: Do not use mutable data structures for argument defaults