Python mutable defaults: why a list survives between calls
By ON Lab · Sources checked November 8, 2026
A Python default argument is evaluated when the function definition executes. A list created there is one object stored with that function, so calls that omit the argument can reuse and mutate the same list. Creating a new list inside the function changes that lifetime. Passing a caller's list is a separate ownership choice: a None fallback does not automatically copy it.
A historical reader asked why successive calls accumulate elements and why the default binds at definition time. The current Python tutorial documents that evaluation rule. This guide explains its observable consequences; it does not claim to establish the language designers' historical motives. We tested small original examples locally, including three boundaries that can otherwise be confused.
Observe when the expression runs
events = []
def make_default():
events.append("default evaluated")
return []
def collect(value, items=make_default()):
items.append(value)
return items
assert events == ["default evaluated"]
first = collect("a")
second = collect("b")
assert first is second
assert first == ["a", "b"]
assert events == ["default evaluated"]
The event appears before either call to collect. Calling collect again does not rerun make_default. Each omitted argument selects the stored default object, and append changes that object. Even first now displays both values because first and second refer to the same list. A printed result from the first call is a view at that moment, not an automatically frozen copy.
This is about evaluation timing and object identity. Python did not store the text of the default expression as a recipe to execute on every call. An ordinary default expression is evaluated in the defining scope when the def statement executes. If that statement executes again, it creates another function and evaluates its defaults again. The lifetime is not necessarily the entire program and is not shared across every unrelated function with the same parameter spelling.
Why mutable values make the rule visible
Every ordinary default follows the evaluation rule; lists make it conspicuous because they can change in place. A tuple containing a list can also expose mutation through that contained list. Merely changing the outer container to a tuple is not a general proof that no state can change.
Distinguish mutating an object from rebinding a local name. Appending to items changes the shared list. Assigning a new list to items makes that local name refer to a different object; it does not by itself replace the function's stored default. The outcome depends on what the body does with the selected value, not just whether the signature contains brackets.
The documented rule explains the behavior consistently without a special list-default exception. It is not evidence that every use of shared defaults is desirable. If shared state is intentional, document its lifetime and ownership explicitly; do not rely on a surprising signature as an undocumented cache.
Create a fresh list only when the argument is omitted or None
def collect(value, items=None):
if items is None:
items = []
items.append(value)
return items
first = collect("a")
second = collect("b")
assert first == ["a"]
assert second == ["b"]
assert first is not second
Here None remains the evaluated default, but the list expression is inside the body and runs on each relevant call. This is appropriate when both omission and explicit None mean “start a new list.” It is a change in the function's contract, so check whether None already carries another meaning in the real API.
Use an identity check, items is None, for this contract. A broad falsiness check would also replace an explicitly supplied empty list. That can silently change whether the caller receives changes to the same object.
A caller's list still belongs to the caller
caller = []
result = collect("c", caller)
assert result is caller
assert caller == ["c"]
The fallback did not run because caller is not None. The function still appends to the supplied list. Our local test verified both the returned identity and the caller's changed contents. Fixing the omitted-argument case does not establish a “never mutates input” guarantee.
If callers expect mutation, this can be correct. If they expect an independent result, make a copy according to the API's accepted input types. Decide deliberately rather than adding copying solely to silence the mutable-default warning.
When None must remain a meaningful input
_MISSING = object()
def collect_copy(value, items=_MISSING):
result = [] if items is _MISSING else list(items)
result.append(value)
return result
caller = []
result = collect_copy("d", caller)
assert caller == []
assert result == ["d"]
This example distinguishes omission with a private sentinel and copies an explicitly supplied iterable. It does not accept None: list(None) raises TypeError. If explicit None has a legitimate meaning, implement that meaning in a separate branch rather than pretending the sentinel example already handles it.
The sentinel itself is created once, intentionally, and identity is its purpose. That illustrates why “all defaults must be newly created on each call” is too broad. A shallow list copy also shares nested objects. If your function mutates nested values, an outer copy alone may not preserve the caller's data; choose and test the ownership contract for those values too.
Verify the real contract before changing production code
- Call without the argument twice and inspect both results after the second call.
- Check object identity as well as equality to reveal aliases.
- Pass an explicit empty list and inspect that original list afterward.
- Test explicit None if the API permits it, and any nested mutable values the body changes.
- Check that intentional caches or stateful callers do not depend on the former behavior.
These checks answer different questions: when a default is created, whether omitted calls share it, and whether explicit input is modified. Keep those outcomes separate in tests and documentation. A fresh omitted-call result is only one of them.
Checked November 8, 2026. The event counter, shared defaults, fresh None fallback, explicit caller mutation and shallow-copy example were verified with Python 3.9.6. The reader's original application, current search demand, and the historical rationale of Python's design were not independently established.