How do you write a Python function with an optional list parameter that starts empty each call?
answer
- Defaults are built once, not per call
- Use a placeholder, not the container
- None means the caller stayed silent
- Rebuild the list inside the body
- Test with is None, not truthiness
basics
~20 sGive the parameter a default of None and build the list inside the body when the caller omitted it. Writing items=[] creates one list while the def statement runs, and every call that omits the argument shares that same list.
solid answer
~40 sThe idiom is `def collect(sku, items=None):` followed by `if items is None: items = []`. Python evaluates a default expression exactly once, while the `def` statement executes, and stores the resulting object on the function object, so with `items=[]` every call that omits the argument is handed the *same* list and an `append` leaks into the next call. `None` is a safe default because it is an immutable singleton with nothing to share, and it is testable by identity with `is`. Use `is None` rather than `if not items:` — a truthiness test also replaces an empty list the caller deliberately passed. Immutable defaults such as numbers, strings, tuples and `frozenset()` need none of this ceremony.
code
python · 9 linesdef collect(sku, items=None):
if items is None:
items = []
items.append(sku)
return items
print(collect("A-1"))
print(collect("A-2"))
print(collect("A-3", ["existing"]))go deeper
Be ready to write the two-line idiom from memory and say why: the default is created once when the def runs, so a list default is shared. Use is None, never a truthiness test.
Explain the mechanism, not just the rule — the default object is evaluated at def time and stored on the function object — and show that it generalizes to dicts, sets and any computed default.
An interviewer expects you to connect this to real incidents: shared caches, timestamps frozen at import, resources opened in a signature. Mention that lint rules catch it in review and that immutable defaults are exempt.
Own the API angle: a signature is a contract, and a mutable default silently makes a function stateful. Decide team-wide whether optional containers default to None or the function returns a new object each time.
### The idiom, in full ```python def collect(sku, items=None): if items is None: items = [] items.append(sku) return items ``` Two lines of ceremony buy you a fresh list on every call that omits the argument. The reason they are needed is a single rule of the language: **a default value expression is evaluated once, while the `def` statement itself executes**, and the resulting object is stored on the function object (in the `__defaults__` tuple for positional-or-keyword parameters, `__kwdefaults__` for keyword-only ones). It is *not* re-evaluated per call. So `def collect(sku, items=[])` builds exactly one list, at definition time, and hands that same list to every caller who leaves the parameter out. An `append` in the body mutates an object that outlives the call, and the next caller sees the previous caller's data. ### Why `None` specifically `None` is the conventional placeholder for three reasons. It is a single immutable singleton, so sharing it between calls is harmless — there is nothing to mutate. It is testable by identity with `is`, which is exact and cannot be fooled by a user type's `__eq__`. And it reads as "no value supplied" to every Python programmer, which matters more than cleverness in a signature that others maintain. The check must be `if items is None`, not a truthiness test: ```python def collect(sku, items=None): items = items or [] # WRONG ``` `items or []` also replaces an **empty list the caller explicitly passed**, quietly discarding their object so the appended item never reaches it. That bug is subtler than the one you were fixing, because it only shows up when a caller happens to pass an empty container. `is None` asks the only question you actually meant to ask: was the argument omitted? ### The same rule applies to every mutable default Lists are the famous case, but the trap is about mutability, not about lists. `def f(cache={})`, `def f(seen=set())`, `def f(buf=bytearray())` and `def f(cfg=Config())` all share one object across calls. Anything whose state can change after creation belongs behind the `None` default. Defaults that are immutable — `0`, `""`, `()`, `None`, `frozenset()`, `True` — are safe to write directly, because an object nobody can change is indistinguishable from a fresh one. A second, related case is a default that is *computed*: `def f(at=time.time())` freezes the moment the module was imported, and `def f(conn=connect())` opens a resource at import time that is then shared forever and typically never closed. The fix is the same shape — default to `None` and do the work inside the body — but the symptom is a stale value or a leak rather than accumulation. ### Variants you will see in real code ```python def collect(sku, items=None): items = [] if items is None else items ``` is the same test written as an expression, and is fine. Some codebases prefer an explicit factory parameter for expensive objects: ```python def build(factory=list): return factory() ``` Here the default is the *type* `list` — an immutable-enough shared object — and the call inside the body produces a new container each time. That is a legitimate pattern when the caller should be able to substitute the container type, and overkill when they should not. If your function needs to tell "argument omitted" from "caller explicitly passed `None`", `None` can no longer be the placeholder; you use a private module-level sentinel object instead. That is a separate design decision and only arises when `None` is itself a meaningful value in the API. ### A note on where the fresh object comes from Constructing inside the body is not a workaround for a language wart; it is the only place a *per-call* object can be created, because the body is the only code that runs per call. Once that clicks, the whole family of related questions answers itself: why a default cache keeps growing, why a default timestamp never moves, why two unrelated call sites are somehow sharing state. They are all the same sentence — the signature runs once, the body runs every time. ### What to say in an interview Say the rule first — defaults are evaluated once, at `def` time, and stored on the function object — then the idiom, then the falsy-test trap, then that it generalizes to dicts, sets and any computed default. Mentioning that a common lint rule flags a mutable default in a signature shows you have seen it caught in review rather than only in a blog post. What an interviewer is checking is not whether you memorized "don't use `[]`", but whether you know *when* Python evaluates the expressions you write in a signature — the same knowledge explains stale timestamps, shared caches and resources opened at import.
- Does the same trap apply to a default of `{}` or `set()`?Yes. The problem is mutability, not lists. A dict, set, bytearray or any instance whose state can change is created once at `def` time and shared by every call that omits the argument. Default them to `None` too, and construct inside the body. Only immutable defaults — numbers, strings, tuples, `None`, `frozenset()` — are safe to write directly in the signature.
- Why is `if items is None:` preferred over `items = items or []`?`items or []` is a truthiness test, so it also fires when the caller explicitly passed an empty list — the function then appends to its own new list and the caller's object never receives anything. `is None` asks the only question that matters: was the argument omitted? It is an identity check, so no user-defined `__eq__` or `__bool__` can answer it for you.
- Is `def collect(sku, items=())` a legitimate alternative?It is safe, because a tuple cannot be mutated, so sharing it between calls is invisible. What you lose is `append`: you must build a new tuple with `items + (sku,)` and return it. That is a reasonable design when the function should not mutate anything, and awkward when accumulating in a loop.
A default value is a prop placed on the stage once before opening night, not a fresh prop handed out at each performance.
saying these in an interview costs you the question
- Claims the empty-list default is recreated on every call
- Explains the bug as a global variable leaking into the function
- Uses `if not items:` so a passed-in empty list is silently discarded
- Fixes it by calling items.clear() at the top of the body
- Thinks the trap also applies to defaults like 0, "" or ()