Python interview question
What is the mutable default argument pitfall in Python?
Short answer
Default values are evaluated once, when the def statement runs, not on each call. A mutable default such as a list or dict is therefore shared across calls and accumulates state. Use None as the default and create the object inside the function. The same applies to defaults like datetime.now().
Answer
In Python, default argument values are evaluated once, when the def statement is executed (usually at import time), and the same object is reused for every call that does not pass that argument. If the default is mutable (a list, dict, set or custom object) and the function modifies it, the changes persist into later calls.
The bug
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("smoke")) # ['smoke']
print(add_tag("ui")) # ['smoke', 'ui'] <- surprise
print(add_tag.__defaults__) # (['smoke', 'ui'],)
The list lives on the function object (__defaults__), so every call that omits tags mutates the same list.
The fix: None as a sentinel
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
Use is None rather than if not tags, because an empty list passed deliberately by the caller is falsy and would be replaced.
If None is a valid value for the argument, use a private sentinel object:
_MISSING = object()
def configure(timeout=_MISSING):
if timeout is _MISSING:
timeout = load_default_timeout()
...
Why it matters in test code
This bug is common in test data builders and helpers, and it causes order-dependent failures that are hard to diagnose:
def build_order(items=[], status="pending"): # shared list!
items.append({"sku": "TSHIRT", "qty": 1})
return {"items": items, "status": status}
def test_one_item():
assert len(build_order()["items"]) == 1 # passes when run first
def test_order_status():
order = build_order()
assert len(order["items"]) == 1 # fails: 2 items, depends on test order
Run alone, each test passes; run together, the second fails. With pytest-xdist or random ordering (pytest-randomly), it becomes intermittent.
Not only mutables: evaluated-once values
Any expression in a default runs once:
from datetime import datetime, UTC
def log(msg, at=datetime.now(UTC)): # timestamp frozen at import time
print(at, msg)
The same applies to uuid.uuid4() or a randomly generated email in a default. Use None and compute inside the function.
Dataclasses guard against it
from dataclasses import dataclass, field
@dataclass
class TestUser:
roles: list[str] = field(default_factory=list)
A dataclass raises ValueError if you write roles: list[str] = [], which is a helpful guard. Pydantic models copy mutable defaults per instance, so the pitfall does not apply there in the same way.
Tooling
Linters catch this: Ruff and flake8-bugbear rule B006 (mutable default) and B008 (function call in default), and Pylint W0102 (dangerous-default-value).
What interviewers look for
- The precise cause: evaluated once at definition time.
- The
Nonesentinel fix withis None. - Awareness of how it causes order-dependent test failures.
- Bonus:
default_factory, timestamps in defaults, lint rules.
A strong answer covers
- Defaults are evaluated once at function definition time
- A mutable default is shared between calls
- Fix: use None as a sentinel and create inside
- Explains mutable vs immutable types
- Mentions other evaluated-once defaults like timestamps or linters that catch it