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.
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
defline 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
- If you were writing a small value type in Python, which dunder methods would you implement, what would you let a dataclass generate, and how does super() decide what to call?mediumAlso on dataclasses4 min
- Adding a second LEFT JOIN doubled the revenue figure on a report. Explain what happened and write the correct query.mediumSame kind of round: coding4 min
- Your test still reaches the network even though you patched the client. Talk me through fixture scope, parametrisation, and where a patch has to point.mediumSame kind of round: concept4 min
- This form marks invalid fields with red text and a red border. What has to change?mediumSame kind of round: concept4 min
- How is `this` determined in JavaScript, and why does a method lose it when you pass it as a callback?mediumSame kind of round: concept4 min
- How do you remove elements from a collection while iterating it, and what makes an iterator fail-fast?mediumSame kind of round: concept5 min
- How would you rewrite a row-by-row pandas transformation, and what is SettingWithCopyWarning telling you?mediumSame kind of round: coding5 min
- Write a query returning the second-highest salary in each department, then tell me what it does when two people tie for the top.mediumSame kind of round: coding3 min