Use None as an omitted-argument marker when None is not valid data, create a fresh list inside the function, and check with is None. Decide separately whether an explicitly supplied list should be mutated or copied.
Predict the list contents after two calls
Run this in a fresh Python process. The output is ['SQL'], then ['SQL', 'Go'], then True, then ['SQL', 'Go']. The function returns its actual history list, not a snapshot. Both first and second refer to that same object, so the second call also changes what first observes.
The default list belongs to this particular function object. Executing the def statement again creates a new function with a new default. This distinction matters for nested functions and for experiments in a notebook: re-running a definition can reset the example and hide the original behavior.
def remember(topic, history=[]):
history.append(topic)
return history
first = remember("SQL")
print(first)
second = remember("Go")
print(second)
print(first is second)
print(first)Create a list only when the argument was omitted
The corrected function below prints ['SQL'] ['Go'] False, followed by ['Python'] True. Calls that omit history get independent lists. A caller that explicitly passes owned gets that same object back after mutation, which is the contract chosen for this example.
Do not replace the condition with if not history or history = history or []. An empty list is false-like, but it can still be a real object supplied intentionally by the caller. Replacing it silently changes ownership behavior. If your API promises no mutation of caller data, copy the supplied collection explicitly and document that choice.
def remember(topic, history=None):
if history is None:
history = []
history.append(topic)
return history
first = remember("SQL")
second = remember("Go")
print(first, second, first is second)
owned = []
result = remember("Python", owned)
print(owned, result is owned)Choose the right default for the API
The rule is about object lifetime and mutation, not a blanket ban on every non-None default. Immutable defaults can be appropriate. An intentional shared cache also has a different contract from a new list per call, but its lifetime, cleanup, and concurrency need deliberate design.
If None is meaningful input, a distinct object() sentinel can distinguish omission from an explicit None. Use identity to compare with that sentinel. The same reasoning applies to defaults such as a timestamp or a generated identifier: decide whether the value belongs to function creation or to each invocation.
| Requirement | Pattern | Check |
|---|---|---|
| Independent list per omitted call | history=None; allocate inside | Two calls return distinct lists |
| Preserve an explicit empty list | if history is None | Returned list is caller's object |
| None is valid input | Private object() sentinel | Omitted and explicit None differ |
| New list per dataclass instance | field(default_factory=list) | Instances do not share the field |
Use default_factory for a dataclass field
A dataclass describes instance fields, so the factory should create a collection for each instance that needs the default. Pass list itself as default_factory; list() would call it immediately and supply a list where a callable is required.
This example prints ['SQL'] [], then False. Mutating first.topics does not affect second.topics. Current dataclasses reject many mutable field defaults rather than quietly accepting the shared-list pattern, so use the documented factory mechanism instead of depending on validation details.
from dataclasses import dataclass, field
@dataclass
class ReviewSession:
topics: list[str] = field(default_factory=list)
first = ReviewSession()
second = ReviewSession()
first.topics.append("SQL")
print(first.topics, second.topics)
print(first.topics is second.topics)Test identity as well as contents
A test that calls remember only once will pass for the broken function. Use two omitted-argument calls, assert their independent contents, and then mutate one result to check that the other stays unchanged. Add an explicit empty-list case so that a future truthiness-based rewrite cannot accidentally change the chosen contract.
For interview practice, explain when the default is evaluated before explaining the output. Then state whether the function mutates caller-owned objects. Those two answers carry over to dictionaries, sets, accumulators, and service helpers. Review the concept in RecallDeck, and keep a small executable regression example alongside your own code.
Quick answers
Frequently asked questions
Are default dictionaries and sets affected too?
Yes. If the function mutates a default dictionary or set, subsequent calls that omit the argument reuse the mutated object belonging to that function.
Why use is None instead of == None?
The contract is an identity check against the None singleton. Equality can be customized by the argument's type and is not needed to detect this marker.
Is copying the default list at the start enough?
It may avoid mutation of that particular default but leaves a shared default object and can obscure ownership. A marker plus explicit allocation makes the intended per-call lifetime clearer.
Source notes
References and review policy
Information checked on October 21, 2026. Section links identify sources for factual claims and technical explanations. Interpretations, practice scenarios and preparation recommendations are RecallDeck’s editorial work.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.