Back to Blog
Python

Python Default Argument Evaluation: The Once-Only Trap

python default argument evaluation: Understand why Python evaluates default arguments once at function definition, and how to avoid the mutable default trap.

default argumentsmutable defaultsfunction definitionPython gotchasparameter evaluation
Diagram showing a Python function with default arguments evaluated once at definition time, contrasting with call time.

python default argument evaluation requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Python's default argument evaluation is a frequent source of confusion for developers coming from languages that evaluate defaults at call time. In Python, default values are evaluated exactly once, when the function definition is executed. This behavior is not a bug, but it becomes one when you use a mutable object as a default and expect a fresh copy on each call.

The Core Behavior: Defaults Are Evaluated Once

Consider this simple function:

def append_to(item, target=[]): target.append(item) return target

If you call append_to(1) and then append_to(2), you might expect each call to start with an empty list. Instead, the second call sees the list already containing 1:

print(append_to(1)) # [1] print(append_to(2)) # [1, 2]

The default list is created when the def statement runs, not when the function is invoked. Every call that omits target reuses the same list object.

The Mutable Default Argument Problem

This behavior becomes a bug when the default is a list, dictionary, set, or any other mutable object. The classic example is a function that accumulates state unintentionally:

def add_item(item, container=[]): container.append(item) return container cart1 = add_item("apple") cart2 = add_item("banana") print(cart1) # ['apple', 'banana'] print(cart2) # ['apple', 'banana']

Both variables point to the same list because the default was never recreated. This violates the principle of least surprise and leads to hard-to-trace state leakage across calls.

Why Python Evaluates Defaults at Definition Time

The reason lies in how Python stores function metadata. When a function is defined, its default values are evaluated and stored in the __defaults__ attribute as a tuple. The function object retains these values for the lifetime of the function.

def f(a=1, b=2): pass print(f.__defaults__) # (1, 2)

Because the defaults are evaluated at definition time, any expression used as a default—like a list literal or a function call—runs once. This is consistent with Python's model where a function definition is an executable statement, not a declaration.

The None Sentinel Pattern

The standard fix for mutable defaults is to use None as the default and create a new mutable object inside the function body when the argument is not provided:

def append_to(item, target=None): if target is None: target = [] target.append(item) return target

Now each call without target gets a fresh list:

print(append_to(1)) # [1] print(append_to(2)) # [2]

This pattern is explicit, works for any mutable type, and is widely used in Python codebases. The None value itself is immutable and safe as a default.

When Mutable Defaults Are Useful

There are legitimate cases where you want a default to persist across calls. For example, a simple memoization cache can be implemented with a mutable default:

def fib(n, cache={0: 0, 1: 1}): if n not in cache: cache[n] = fib(n - 1) + fib(n - 2) return cache[n]

Here the dictionary is intentionally shared across calls to avoid recomputation. Similarly, a function that registers callbacks might use a list default to collect handlers. In these cases, the behavior is deliberate, but it should be documented clearly because it violates the usual expectation of fresh defaults.

Alternatives and Design Considerations

If you need a fresh mutable object per call, the None sentinel is the simplest approach. For more complex scenarios, consider these alternatives:

  • Keyword-only arguments: Use * to force callers to pass certain arguments explicitly, reducing the chance of accidental default reuse.
  • dataclasses.field(default_factory=list): When defining data classes, default_factory creates a new instance per object, avoiding the shared-default problem.
  • Immutable defaults: Use tuples or frozensets if you don't need mutation. They are safe as defaults because they cannot be modified in place.
from dataclasses import dataclass, field @dataclass class ShoppingCart: items: list = field(default_factory=list)

Compatibility and Maintainability Concerns

The once-only evaluation behavior is consistent across all Python versions, so code relying on it will not break when upgrading. However, it is a common source of bugs for developers new to Python. In code reviews, flag any mutable default argument and ask whether the shared state is intentional. If it is not, suggest the None sentinel pattern.

When writing APIs, prefer defaults that are immutable or use the sentinel pattern to make the function's behavior predictable. This reduces the cognitive load for callers and prevents subtle state leakage that can appear only under specific call sequences. The cost of a few extra lines in the function body is worth the clarity it brings to the function's contract.

python default argument evaluation: Practical Usage and Code | RYUSLOG DEV