Skip to content
QSWEQB
mediumConceptCodingEntryMidSenior

Why does a mutable default argument keep the values from previous calls?

Default expressions are evaluated once while the def statement runs and the resulting objects are stored on the function object in __defaults__, so every call that omits the argument shares the same list or dict and sees every prior mutation.

2 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate locate the evaluation at definition time rather than describing it as caching or memoisation
  • Whether the fix uses an explicit identity check against a sentinel instead of a truthiness check
  • That they can name where the defaults live on the function object, not just narrate the symptom
  • Whether the same evaluate-once rule is recognised in class attributes and dataclass fields
  • Does the candidate see that definition-time evaluation also freezes captured state such as a timestamp or a config lookup

Answer

Defaults belong to the function object

def is a statement that executes, not a declaration the interpreter merely notes. When Python runs it, each default expression is evaluated once, at that moment, and the resulting objects are attached to the function object it creates. You can read them straight back: __defaults__ is a tuple holding the defaults for positional-or-keyword parameters, and __kwdefaults__ is a dict for keyword-only ones. A call that omits the argument does not re-evaluate anything; it binds the parameter name to the object already sitting in that tuple.

With an immutable default this sharing is invisible. def f(n=0) hands the same 0 to every call and nobody minds, because there is no way to change 0. A mutable default shares in exactly the same way, but now the object you were handed is the object every earlier call mutated.

def add_item(item, basket=[]):
    basket.append(item)        # mutates the one list stored on the function
    return basket

add_item("apple")              # ['apple']
add_item("pear")               # ['apple', 'pear'] - the same list
add_item.__defaults__          # (['apple', 'pear'],)

The sentinel fix

The fix is to make the default something immutable and construct the real value inside the body, where the code runs per call.

def add_item(item, basket=None):
    if basket is None:         # identity, not truthiness
        basket = []            # a fresh list on every call
    basket.append(item)
    return basket

Use is None rather than if not basket. An empty list or an empty string that the caller passed deliberately is falsy, and a truthiness check would silently discard it and substitute a new object. When None is itself a meaningful value the caller might pass, create a private sentinel with _MISSING = object() and compare against that instead.

Why "Python caches it" is the wrong explanation

Calling this caching or memoisation is the answer that sounds informed and fails the follow-up. It implies an optimisation, which invites the question of how to switch it off, and there is nothing to switch off: no state is being remembered, because only one object was ever created. Mutating it is ordinary mutation of a long-lived object, and an interviewer will check that you understand this by asking what __defaults__ contains after two calls, or by asking what def log(at=time.time()) records. That timestamp is fixed at import, and no amount of re-calling will refresh it.

The same rule elsewhere

A class body also executes once, so tags = [] at class level is one list shared by every instance, and self.tags.append(x) mutates it for all of them. Assign it in __init__ if each instance needs its own.

Dataclasses inherit the problem because a field default becomes a class attribute, so tags: list[str] = [] raises a ValueError at class-creation time rather than letting you discover it in production. The replacement is field(default_factory=list): a zero-argument callable that the generated __init__ invokes per instance. Note that the guard is not exhaustive, since it catches the common unhashable types; a custom mutable class of your own is accepted as a default and shares itself across every instance exactly as a bare list would.

Nothing is cached and nothing is remembered. There is one object, created when the def line ran, and moving construction into the body is what gives each call its own.

Likely follow-ups

  • When is a mutable default deliberately useful, for instance as a memoisation cache?
  • Why is None a poor sentinel when the caller may legitimately pass None, and what do you use instead?
  • Why does a dataclass raise an error for a list default but accept an arbitrary mutable object?
  • How would you write a decorator that gives each call a fresh copy of the declared default?

Related questions

Further reading

default-argumentsmutabilitydata-modeldataclasses