The Python Mutable Default Argument Problem
python mutable default argument problem: Understand why mutable default arguments in Python persist across calls and learn the standard None-sentinel fix to avoid shar...
The python mutable default argument problem is one of the most common sources of subtle bugs for developers moving from other languages. It appears when a function uses a mutable object—like a list, dictionary, or set—as a default parameter value. The behavior is surprising: the default value is shared across all calls to the function, and any mutation inside the function persists across invocations.
Consider this minimal example:
def add_item(item, container=[]): container.append(item) return container print(add_item("a")) # ['a'] print(add_item("b")) # ['a', 'b']
The second call returns ['a', 'b'] instead of ['b']. The same list object is reused every time. This happens because Python evaluates default arguments once at function definition time, not each time the function is called.
Why Python Evaluates Default Arguments Only Once
When you define a function, Python creates a function object and evaluates each default expression at that moment. The resulting objects are stored as attributes on the function, typically in __defaults__. For immutable defaults like integers or strings, this is harmless because you can't mutate them. But for mutable objects, the function holds a reference to the same object across all calls.
def append_to(element, target=[]): target.append(element) return target print(append_to.__defaults__) # ([],)
The empty list in __defaults__ is the same list used in every invocation. When you call append_to, the default value is bound to the parameter target, and any in-place mutation modifies that shared list.
This is not a bug in Python's design; it's a consequence of the evaluation model. The language deliberately evaluates defaults once for performance and simplicity. The problem arises when developers assume defaults are evaluated per call, as in many other languages.
The Standard Fix: Using None as a Sentinel
The idiomatic solution is to use None as the default and create a fresh mutable object inside the function body. This avoids shared state because a new list is created on each call.
def add_item(item, container=None): if container is None: container = [] container.append(item) return container print(add_item("a")) # ['a'] print(add_item("b")) # ['b']
The None sentinel is immutable and safe as a default. The function checks for None and assigns a new list. This pattern works for any mutable type.
Handling Dictionaries, Sets, and Other Mutable Types
The same fix applies to dictionaries, sets, and any other mutable object. For example:
def add_key_value(key, value, mapping=None): if mapping is None: mapping = {} mapping[key] = value return mapping def add_to_set(item, collection=None): if collection is None: collection = set() collection.add(item) return collection
In each case, the default is None, and the actual mutable object is created only when the function is called without that argument. This guarantees that each call gets an independent container.
When a Mutable Default Is Actually Acceptable
There are rare cases where using a mutable default is intentional. For example, if you want to cache results across calls, a shared list or dict can serve as a memoization store. However, this is almost always a design smell because it couples the function's behavior to call order and makes the API surprising.
A safer alternative is to use a module-level constant or a class attribute explicitly, making the shared state visible. For instance:
_CACHE = {} def get_data(key): if key not in _CACHE: _CACHE[key] = fetch_from_source(key) return _CACHE[key]
Here, the shared dictionary is intentional and documented. The function does not mutate a default argument; it uses a named global. This is clearer than hiding shared state in a default parameter.
Avoiding the Problem in Function Signatures and API Design
Beyond fixing individual functions, you can prevent the problem by following a simple rule: never use a mutable literal as a default. When designing APIs, prefer None as a placeholder and document that the parameter is optional. This makes the function's behavior predictable and avoids surprising side effects for callers.
If you are reviewing code, watch for default arguments like =[], ={}, or =set(). They are almost always a mistake unless the function is explicitly designed to share state. Replacing them with None and an in-function initialization is a low-risk change that improves maintainability.
A Note on Performance and Memory
The None sentinel pattern does not introduce meaningful overhead. The if container is None check is cheap, and creating a new list or dict on each call is typically negligible compared to the work the function performs. In fact, the shared-state version can cause memory leaks if the list grows unboundedly across calls, because the function object retains a reference to it. The sentinel approach avoids that risk entirely.
There is no need to optimize for the rare case where a mutable default might save a few microseconds of allocation. Correctness and predictability matter more. If you ever need to share state, do it explicitly with a global or class attribute, not through a default argument.