Backend / Python core / Tricky questions / 01_mutable_default_arguments.md

Mutable default arguments

Updated 3 interview angles 1 min read source
On this page6
  1. The gotcha
  2. Minimal repro
  3. Why it happens
  4. How to avoid
  5. Related
  6. Interview angle

Mutable default arguments

The gotcha

Default argument values are evaluated once, at function definition time, and the same object is reused across calls.

Minimal repro

python
def append_to(item, target=[]):
    target.append(item)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2]   ← surprise: same list reused
print(append_to(3))  # [1, 2, 3]

Why it happens

When Python compiles def, it evaluates [] once and stores the resulting list object in func.__defaults__. Every call that omits target binds the parameter to that same shared object. Mutating it via append mutates the default itself.

python
print(append_to.__defaults__)  # ([1, 2, 3],)

How to avoid

Use None as the sentinel and create the mutable object inside:

python
def append_to(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

For dataclasses, use field(default_factory=list) — same problem in a different shape (see 18_dataclass_mutable_default.md).

Interview angle 3

  • “What does this print?” - given def add(item, target=[]) called twice, the second call returns both items. The default is evaluated once, when the function is defined, so every call without an explicit argument shares one list.
  • “How do you fix it?” - target=None and if target is None: target = [] inside the body, so a fresh list is created per call. The sentinel is the idiom precisely because None is immutable and unambiguous.
  • “Is it ever deliberate?” - occasionally, as a cheap memo cache: def f(x, cache={}) genuinely works because the dict persists. It is fragile - the cache is unbounded, shared, and invisible in the signature - so functools.lru_cache is the version you should write.