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

  1. Any =[], ={} or =set() in a signature is a bug until proven otherwise.
  2. Default to None, build the object inside.
  3. Compare with is None.
  4. Turn on the B006 lint rule.

Have you ever shipped this bug, and how long did it take you to find it?