Why does `def record_bid(price, log=[])` keep values from earlier calls?
answer
- When does the def statement actually run?
- The default is created once, not per call
- The object lives on the function itself
- Check the function's __defaults__ tuple
- Sentinel None, build the list inside
basics
~20 sThe empty list is built once, when the def statement executes at import time, and stored on the function object. Every call that omits log mutates that same list. Default to None and build a fresh list inside the body.
solid answer
~50 sDefault values are evaluated **once**, when the `def` statement runs — not on each call. The resulting objects are stored on the function object and are visible as `record_bid.__defaults__`. Because the default here is a mutable `list`, every call that omits `log` appends to the *same* list, so the second call sees the first call's bid. It is not a scoping quirk; the list simply outlives the call. Immutable defaults such as `0`, `""`, `()` or `None` never show the problem, because nothing can mutate them. The idiomatic fix is the `None` sentinel: `def record_bid(price, log=None):` then `if log is None: log = []` in the body, which allocates a new list per call while still letting a caller pass their own. The same rule covers `dict`, `set` and any mutable object built by the default expression, including a call such as `datetime.datetime.now()` that freezes a timestamp at import time.
code
python · 7 linesdef record_bid(price, log=[]):
log.append(price)
return log
print(record_bid(120))
print(record_bid(340))
print(record_bid.__defaults__)go deeper
Recall the symptom and the fix: a mutable default is shared across calls, so default to None and create the container inside the function. Being able to name the trap when you see it is enough at this level.
Explain the timing precisely: the def statement executes the default expressions once and stores the objects on the function, reachable as __defaults__. Extend the point past lists to dicts, sets and computed defaults such as a timestamp.
Show how you keep it out of a codebase rather than just explaining it: a lint rule on mutable defaults, the sentinel pattern when None is a legitimate value, and the debugging tell of state surviving across unrelated calls.
Frame it as an API-design decision. Whether a function allocates its own container or accepts a caller's is a contract choice about ownership and aliasing, and it should be consistent across a codebase rather than decided per function.
## What actually happens A `def` statement is executable code, not a declaration. When the interpreter reaches it — usually while importing the module, at process start — it evaluates the default expressions **once**, builds a function object, stores the results in it, and binds the name. From then on the defaults are fixed objects attached to the function: ```python def record_bid(price, log=[]): log.append(price) return log record_bid(120) # [120] record_bid(340) # [120, 340] <- same list record_bid.__defaults__ # ([120, 340],) ``` `__defaults__` is the whole story in one line: the tuple of default objects lives on the function, so the list has the lifetime of the function, not of a call. Every call that omits `log` gets that one list and appends to it. Nothing is being "remembered" by magic; the object was simply never re-created. With an immutable default the behaviour is identical but invisible. `def f(n=0)` also evaluates `0` exactly once and stores it, but no operation can change the integer `0`, so no state accumulates. That is why the trap only ever shows up with `list`, `dict`, `set`, a mutable instance of your own class, or an expression whose value should have been recomputed. ## The second, quieter failure The same rule bites when the default is a *computed* value rather than a container: ```python import datetime def stamp(event, at=datetime.datetime.now()): # frozen at import time return event, at ``` Every call reports the moment the module was imported, which on a long-running process is wrong by hours. This variant is worth naming in an interview because it shows you have understood the rule (defaults are evaluated once, at `def` time) rather than memorised the list case. ## The fix The conventional repair is the `None` sentinel: ```python def record_bid(price, log=None): if log is None: log = [] log.append(price) return log ``` Now a fresh list is created per call, and a caller who *wants* to accumulate into their own list can still pass one. Use `is None`, not a truthiness test: `if not log` would also replace an empty list the caller deliberately handed you. When `None` is itself a meaningful value for the parameter, use a private sentinel object instead: ```python _MISSING = object() def record_bid(price, log=_MISSING): if log is _MISSING: log = [] ... ``` An alternative that reads well for small containers is an immutable default plus a conversion: `def f(tags=()):` then `tags = list(tags)` in the body. It is safe because the default cannot be mutated, and it also relaxes the parameter to accept any iterable. ## Why Python does not simply re-evaluate Re-evaluating defaults per call would make every call pay for the expression and would make the semantics of a default depend on the caller's environment. Evaluating once also enables a well-known idiom: binding a value into a function at definition time, as in `lambda item, key=key: ...` inside a loop, where the default captures the current loop value rather than the variable. So the behaviour is deliberate, documented, and exploited on purpose — it only surprises you when the object it froze is mutable. ## Spotting and preventing it The pattern is mechanical, so it is worth catching mechanically. A mutable literal in a parameter list is a fixed shape that a linter flags, and it is one of the cheapest rules to switch on in a code-review checklist. In review, treat any `=[]`, `={}`, `=set()` or function call in a signature as a defect until proven otherwise. In debugging, the tell is state that survives across independent calls, and `f.__defaults__` confirms it in one line at the prompt. One clarification that often comes up: this has nothing to do with closures, globals or class attributes, and it is not affected by whether the parameter is keyword-only. It is purely about *when* the default expression runs and *what kind of object* it produced.
- Which default values are safe to write as literals in a signature?Anything immutable: numbers, `True`/`False`, `None`, `str`, `bytes`, an empty `tuple`, a `frozenset`. They are still created once, but nothing can change them, so the sharing is invisible. Treat any mutable literal or any function call in a parameter list as suspect.
- Is `def stamp(at=datetime.datetime.now())` affected by the same rule?Yes, and more subtly. The call runs once when the module is imported, so every invocation reports the process's start time rather than now. The fix is the same sentinel shape: default to `None` and call `datetime.datetime.now()` inside the body when the argument was omitted.
- If defaults were evaluated per call instead, what would you lose?The definition-time capture idiom. Writing `key=key` as a default binds the current value at definition time, which is how you snapshot a loop variable into a callable instead of closing over the variable itself. Per-call evaluation would also re-run the expression on every call and make its result depend on the caller's environment.
saying these in an interview costs you the question
- Says the default list is recreated on every call
- Blames a global variable or a closure
- Calls it a bug in Python rather than defined behaviour
- Fixes it with `if not log` instead of `if log is None`
- Claims making the parameter keyword-only avoids the problem
- Thinks only lists are affected, not dicts or computed defaults