Why is the empty dictionary a dangerous default value in Python?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
One of the most common sources of silent bugs in Python is using a mutable object like {} or [] as a default argument in a function signature. The function appears to work correctly on the first call, then produces baffling results on the second. Understanding why this happens requires knowing a single rule about how Python evaluates default arguments.
The core issue is that Python evaluates default parameter values exactly once, at function definition time, not each time the function is called. This means every call that relies on the default shares the same object in memory. For immutable defaults like None, 0, or "" this is harmless. For mutable defaults like dictionaries and lists, it creates shared state between calls that was almost certainly not intended.
The Bug in Action
Consider a function that is supposed to give each user a fresh dictionary of settings.
Both user1 and user2 point to the exact same dictionary. When Bob's name was written into settings, it overwrote Alice's name because they share one underlying object. This is not a copy issue; it is an identity issue.
The Same Problem with Lists
The trap is not limited to dictionaries. Any mutable default exhibits the same behavior.
Each call was supposed to start with an empty list, but instead the list grows across calls because the same list object is reused every time.
Why Python Works This Way
Python functions are objects, and their default values are stored as attributes of the function object at definition time. This is a deliberate design choice that makes function creation fast (defaults are computed once, not on every call) and allows useful patterns like memoization caches stored in default arguments. However, it creates the mutable default trap as a side effect.
Verifying with __defaults__
You can inspect the stored default values directly to confirm they are shared.
The __defaults__ tuple holds the actual objects used as defaults. Mutating the returned dictionary mutates the default itself because they are the same object.
The Fix: Use None as a Sentinel
The standard Python idiom is to use None as the default and create a fresh mutable object inside the function body.
Now each call that does not pass an explicit settings argument gets its own brand-new dictionary. Callers who do pass a dictionary still get the expected behavior of modifying their own object.
The same pattern works for lists and any other mutable type.
Why Immutable Defaults Are Safe
Immutable types like int, str, tuple, and frozenset cannot be modified in place, so sharing them across calls is harmless.
The string "Hello" is the same object on every call, but since strings cannot be mutated, there is no way for one call to affect another. The "danger" only exists when the default can be changed in place.
Linting and Prevention
Most modern linters catch this mistake automatically.
Enabling these checks in your CI pipeline is the most reliable way to prevent the bug from reaching production. The B006 rule from flake8-bugbear is particularly thorough, catching not just {} and [] but also set(), dict(), and list() calls used as defaults.
Common Pitfalls
- Assuming each call gets a fresh copy of the default mutable object. It does not; all calls share the same instance.
- Using
dict()orlist()instead of{}or[]as the default does not help. The call is still evaluated once at definition time, producing one shared object. - Forgetting to apply the fix to all mutable defaults in a function. If a function has both a default dict and a default list, both need the
Nonesentinel pattern. - Intentionally using a mutable default as a cache without documenting it. This is a valid advanced pattern but confuses other developers who will "fix" it.
- Testing only with a single function call hides the bug because the problem only manifests on the second and subsequent calls.
Summary
- Python evaluates default arguments once at function definition time, not on each call.
- Mutable defaults like
{}and[]are shared across all calls that use them, causing unintended state leakage. - Inspect
func.__defaults__to see the stored default objects and verify whether they have been mutated. - The standard fix is to use
Noneas the default and create the mutable object inside the function body. - Immutable defaults (
int,str,tuple,frozenset) are safe because they cannot be modified in place. - Enable linter rules like
W0102(pylint) orB006(flake8-bugbear) to catch this mistake automatically.

