Python
programming
default values
empty dictionary
best practices

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.

python
1def create_user(name, settings={}):
2    settings["name"] = name
3    return settings
4
5user1 = create_user("Alice")
6print(user1)  # {'name': 'Alice'}
7
8user2 = create_user("Bob")
9print(user2)  # {'name': 'Bob'}
10
11print(user1)  # {'name': 'Bob'}  <-- Alice's dict was mutated!
12print(user1 is user2)  # True  <-- same object

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.

python
1def add_item(item, items=[]):
2    items.append(item)
3    return items
4
5print(add_item("apple"))   # ['apple']
6print(add_item("banana"))  # ['apple', 'banana']  <-- leaked state
7print(add_item("cherry"))  # ['apple', 'banana', 'cherry']

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.

python
1def buggy(data={}):
2    return data
3
4print(buggy.__defaults__)  # ({},)
5
6buggy()["key"] = "value"
7print(buggy.__defaults__)  # ({'key': 'value'},)
8
9# The default has been permanently mutated
10result = buggy()
11print(result)  # {'key': 'value'}  <-- not empty!

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.

python
1def create_user(name, settings=None):
2    if settings is None:
3        settings = {}
4    settings["name"] = name
5    return settings
6
7user1 = create_user("Alice")
8user2 = create_user("Bob")
9
10print(user1)  # {'name': 'Alice'}
11print(user2)  # {'name': 'Bob'}
12print(user1 is user2)  # False  <-- separate objects

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.

python
1def add_item(item, items=None):
2    if items is None:
3        items = []
4    items.append(item)
5    return items
6
7print(add_item("apple"))   # ['apple']
8print(add_item("banana"))  # ['banana']  <-- fresh list each time

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.

python
1def greet(name, greeting="Hello"):
2    return f"{greeting}, {name}!"
3
4print(greet("Alice"))  # Hello, Alice!
5print(greet("Bob"))    # Hello, Bob!

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.

python
1# pylint: W0102 (dangerous-default-value)
2# flake8-bugbear: B006 (mutable default argument)
3# mypy with plugins can also flag this pattern
4
5# Running pylint on the buggy function:
6# W0102: Dangerous default value {} as argument (dangerous-default-value)

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() or list() 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 None sentinel 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 None as 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) or B006 (flake8-bugbear) to catch this mistake automatically.

Course illustration
Course illustration

All Rights Reserved.