The DoesItHold field notes

Mutable defaults keep state between Python calls

A default argument is evaluated when the function is defined, so one list can be reused across calls.

DoesItHold Editorial · · reviewed · guide-python-defaults-001

Direct answer

Use None as the sentinel and allocate the mutable value inside the function.

Python evaluates default expressions once, when the def statement runs. A list, dictionary or set created there becomes part of the function object and is reused whenever the caller omits that argument. Mutating it changes what a later call receives.

The surprising state is therefore deterministic, not random. Reproduce it with two calls that omit the argument, then compare both returned values and their identities. That small experiment separates a shared-default defect from state stored elsewhere.

Trace the failure
  1. 01
    Definitionone default list is allocated
  2. 02
    Call Athe shared list becomes ['a']
  3. 03
    Call Bthe same list becomes ['a', 'b']
  4. 04
    SentinelNone creates a fresh list per call
The defect is object reuse: both omitted arguments point to the list created when def ran. The sentinel moves allocation into each call.

Minimal example

Scrollable code region

Buggy
def collect(value, items=[]):
    items.append(value)
    return items
Corrected
def collect(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

What changes: Two omitted calls no longer share an object; a collection explicitly supplied by the caller is still mutated as requested.

Debugging method

Call the function twice without the argument, compare object identities and move allocation into the call path.

  1. Reproduce with two calls that omit the mutable argument.
  2. Inspect the function default through __defaults__ and compare object identities.
  3. Replace the mutable default with None and allocate only when the argument is None.
  4. Test both omitted input and an explicitly supplied empty collection.

Common wrong fix

Copying the list at the return statement still permits mutation during the call and obscures ownership. Using items = items or [] also replaces a valid empty list supplied by the caller.

Limits and edge cases

A persistent mutable default can be intentional for caching, but that state is implicit, shared and difficult to reset. Prefer an explicit cache object or a standard caching decorator so ownership and lifecycle remain visible.

Sources