Why are mutable default arguments dangerous in Python?
In Python, default argument values are evaluated **once** when the `def` statement is executed — not each time the function is called. For immutable defaults like integers, strings, or `None`, this is harmless because they cannot be modified in place. For mutable defaults like lists, dicts, or sets, this becomes a subtle and persistent bug.
The canonical example:
```python def append_to(element, to=[]): to.append(element) return to
append_to(1) # [1] append_to(2) # [1, 2] — not [2]! append_to(3) # [1, 2, 3] ```
All three calls share the same list object. The default `[]` is stored in `append_to.__defaults__[0]` and lives for the lifetime of the function object. Each mutation persists across calls because no new list is created.
Why does Python work this way? Default values are computed at definition time for performance and to allow complex objects (like compiled regexes) to be cached as defaults. It is a deliberate design decision, not an oversight — but it consistently surprises developers.
The idiomatic fix is the **sentinel pattern**: use `None` as the default and create the mutable object inside the function body:
```python def append_to(element, to=None): if to is None: to = [] to.append(element) return to ```
Now each call that omits `to` gets a fresh list. Callers who explicitly pass a list get mutation on that list, which is expected.
The same bug applies to dicts and custom objects:
```python def add_user(name, cache={}): cache[name] = True return cache ```
Python dataclasses handle this correctly by requiring `field(default_factory=list)` for mutable defaults and raising a `ValueError` if you try to assign a mutable default directly. This is an intentional guardrail.
The pattern is also intentionally exploited in rare cases: a mutable default as a function-level cache (poor man's `functools.lru_cache`). But this is an advanced idiom, not a recommendation for general use.
In code review, any mutable default argument should be flagged. Linters like `flake8` (B006 via `flake8-bugbear`) catch this automatically.
Correctly explains that defaults are evaluated once at definition time, shows the list mutation bug, and gives the None sentinel fix.
Covers definition-time evaluation, explains __defaults__ storage, shows the fix, mentions dataclass's field(default_factory=...) guardrail, and knows the deliberate cache exploitation pattern.
Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.
Get Graded — Free Assessment