I called the same function twice and the second call returned the first call's data. No global variable, no cache, nothing obvious. The cause was one line in the function signature.
The bug
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] <- expected ['b']
print(add_item("c")) # ['a', 'b', 'c']
Every call without an items argument shares the same list.
Why it happens
Python evaluates default argument values once, when the def statement runs, not each time the function is called. The list is created a single time and stored on the function object. You can see it:
print(add_item.__defaults__) # (['a', 'b', 'c'],)
The Python docs describe this in the section on default argument values: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values
This affects any mutable default: lists, dicts, sets, and also objects such as datetime values you meant to be "now".
The fix
Use None as the default and create the object inside the function:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['b']
Use is None, not if not items. An empty list the caller passes in on purpose is falsy, and you would silently replace it with a new one, which breaks callers who expect their own list to be filled.
The same trap with dicts and dataclasses
def log(event, ctx={}): # shared dict, same bug
ctx["last"] = event
return ctx
Dataclasses refuse this pattern for lists and dicts and raise a ValueError for mutable defaults. Use field(default_factory=list) instead: https://docs.python.org/3/library/dataclasses.html#mutable-default-values
How to catch it early
- Linters flag it. Ruff has rule B006 (mutable argument default), and flake8-bugbear has the same check.
- When a function seems to "remember" state, print
func.__defaults__first.
When the shared default is actually useful
Some people use it on purpose as a cheap memo cache. It works, but it is surprising to the next reader. A named module-level dict or functools.lru_cache says what you mean.
Quick checklist
- Any
=[],={}or=set()in a signature is a bug until proven otherwise. - Default to
None, build the object inside. - Compare with
is None. - Turn on the B006 lint rule.
Have you ever shipped this bug, and how long did it take you to find it?
Top comments (0)