Why is the empty dictionary a dangerous default value in Python?
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:
To remedy this situation, use None
as a default and initialize the dictionary within the function:
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. UseNone
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.
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.
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.
Was this article helpful?