Explain Codes LogoExplain Codes Logo

Why is the empty dictionary a dangerous default value in Python?

python
mutable-defaults
function-calls
defensive-programming
Nikita BarsukovbyNikita Barsukov·Mar 13, 2025
TLDR

Assigning a mutable object like a dict() as a default parameter in a function can lead to uncaught bugs. The object is created once when the function is defined and then shared across different function calls. Any modifications to this object persist across these calls.

Here's a brief example:

def add_item(key, value, data={}): data[key] = value return data add_item('a', 1) # {'a': 1}, but the party is just starting add_item('b', 2) # {'a': 1, 'b': 2}, the plot thickens

To remedy this situation, use None as a default and initialize the dictionary within the function:

def add_item(key, value, data=None): if data is None: data = {} data[key] = value return data

Now, each call creates a new dictionary, ensuring each function call is an island, isolated from the rest.

The pitfalls of mutable defaults

When you use mutable objects like dictionaries or lists as default values, they remain persistent between function calls. If these objects are modified in a function call, those modifications are carried over to subsequent function calls. This behaviour is counter-intuitive, leading to hard-to-track bugs.

Key insights

  • Immutable objects as defaults: Safe and consistent, they're not modifiable.
  • Mutable objects as default: Potential minefield, as their state persists across different function calls.
  • Use None as the default: A widely used approach. Use None and initialize the mutable object within the function body.

Proven practices

  • Initialize mutable objects within the function.
  • Default to immutable objects, like None, to signify no value.
  • If you're breaking the norm, good documentation is critical for maintaining readable code.

Protective gear against default value mishaps

functools.partial to the rescue

The functools.partial function can be employed to create pre-configured function objects with mutable defaults calculated at runtime.

from functools import partial def add_item(key, value, data): data[key] = value return data # Now we spawn a new dictionary every time! add_item_with_empty_dict = partial(add_item, data={})

Playing defense

When writing library code, use defensive programming. This ensures that defaults are not just safe but are immune to unsolicited exploitation of shared mutable defaults.

Immutable all the way

In cases where an immutable mapping would serve better, consider using a frozendict. This requires a library that provides it, though.

Prevention and alternatives to mutable defaults

The "None" safe-zone

The most used strategy is to default to None and use the function's body to create mutable objects.

Factories or callable defaults

An alternative approach involves using factory functions or callable defaults that yield a new mutable object each time the function is called.

def add_item(key, value, data=lambda: {}): data = data() data[key] = value return data

Call the constructors

With classes that emulate mutable containers, use the type constructor itself as a default value. Invoke the callable within the function to create a fresh instance each time the function is called.

Spotless clean-ups

For functions that genuinely need to maintain state across calls, ensure that they include a method to clear or reset this state when necessary.

def add_item(key, value, data={}): if 'clear' in key: # 'Clear' signal detected, initiate retreat! data.clear() else: data[key] = value return data