Fixing the Python Mutable Default Argument Trap
python mutable default argument: Why Python evaluates mutable default arguments once, how shared list or dict state leaks across calls, and the standard None-sentinel...
python mutable default argument requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The Surprising Behavior of Mutable Defaults
def add_item(item, bucket=[]): bucket.append(item) return bucket print(add_item("first")) # ['first'] print(add_item("second")) # ['first', 'second']
The second call returns a list that already contains "first" even though no bucket argument was passed. The default list was created once, when the function was defined, and the same object is reused for every call that omits the argument. This is the classic python mutable default argument problem, and it produces state that silently leaks from one call to the next.
The same behavior appears with dicts, sets, bytearrays, and any custom mutable object used as a default.
Why Python Evaluates Defaults Once at Definition Time
When the def statement executes, Python evaluates each default expression exactly once and stores the resulting objects in the function's __defaults__ tuple. This evaluation happens when the module is imported, not when the function is called.
def build(bucket=[]): return bucket print(build.__defaults__)
The list stored in __defaults__ is the same object returned on every call. Because lists are mutable, an in-place operation such as append persists across calls. The default is not re-evaluated per call, so each omitted argument reuses the same object.
This is not a language bug. It is a direct consequence of evaluating defaults once at definition time, and the behavior is fully predictable once the rule is understood.
The Standard Fix: None as a Sentinel
The conventional fix is to use None as the default and create a fresh mutable inside the function body.
def add_item(item, bucket=None): if bucket is None: bucket = [] bucket.append(item) return bucket
Each call that omits bucket gets a brand-new list. Calls that pass a list explicitly still use that list, so the function keeps its flexibility.
The None check is cheap and idiomatic. It composes cleanly with type hints:
def add_item(item: str, bucket: list[str] | None = None) -> list[str]: if bucket is None: bucket = [] bucket.append(item) return bucket
The same pattern applies to dicts, sets, and any other mutable default.
When the Shared Default Is Actually Useful
There are rare cases where sharing a mutable default is intentional. A registry or cache that should persist across all calls is one example:
def register(name, registry={}): registry[name] = True return registry
This works, but it hides shared state inside the function signature. A module-level variable communicates the intent more clearly:
_registry = {} def register(name): _registry[name] = True return _registry
If shared state is genuinely required, make it explicit. Relying on the mutable default as a hidden singleton confuses readers, complicates testing, and makes the state hard to reset between test cases.
Dataclasses and Default Factory
The same problem appears with dataclasses, but the dataclass machinery rejects a mutable default at class definition time.
from dataclasses import dataclass, field @dataclass class Bucket: items: list = [] # raises ValueError at class definition time
Use field(default_factory=list) instead:
@dataclass class Bucket: items: list = field(default_factory=list)
default_factory is invoked once per instance, so each Bucket gets its own list. This is the same principle as the None sentinel, but enforced by the dataclass API rather than by convention.
Runtime and Import-Time Implications
Because defaults are evaluated at definition time, the default expression runs during module import. That matters when the expression has side effects or depends on runtime state.
def now(timestamp=time.time()): return timestamp
Here time.time() runs once at import, not at each call. Every call returns the same timestamp. To get the current time per call, compute it inside the body.
The same rule applies to keyword-only defaults, which are stored in __kwdefaults__. The fix is identical regardless of how the parameter is declared.
Debugging and Maintainability Concerns
The mutable default argument is a common source of subtle bugs in production code. The symptom is often intermittent: a function returns data that includes entries from an earlier request, and the cause is not obvious from reading the call site.
When reviewing code, a mutable default is a strong signal that object lifetime may not have been considered. The fix is small, but the failure mode is confusing because the bug only appears when the argument is omitted.
Testing is also affected. If a function with a mutable default is called across multiple test cases without passing the argument, state leaks between tests. The None sentinel pattern keeps each test isolated, and the default_factory pattern does the same for dataclass instances.