backend / python core / tricky questions / 44_dict_fromkeys_shared_default.md

dict.fromkeys shares a single default object

3 min read source

dict.fromkeys shares a single default object

The gotcha

dict.fromkeys(keys, default) evaluates default once and assigns the same object to every key. If default is mutable, mutating one entry mutates all of them.

Minimal repro

d = dict.fromkeys(['a', 'b', 'c'], [])
d['a'].append(1)
print(d)        # {'a': [1], 'b': [1], 'c': [1]}    ← all share the same list

The empty list literal [] is created once, before dict.fromkeys runs. The dict ends up with three keys all referencing the same list. Append to one → visible everywhere.

Why it happens

This is the same trap as mutable default arguments. dict.fromkeys doesn’t have a “factory” parameter — it takes a value. Python evaluates the value expression once, passes the result, and the method assigns the same reference to every key.

# Conceptually:
def fromkeys(keys, default=None):
    return {k: default for k in keys}        # `default` is the same object for all

Variants and confusion

For immutable defaults (the common case), no problem:

dict.fromkeys(['a', 'b', 'c'], 0)        # {'a': 0, 'b': 0, 'c': 0}
dict.fromkeys(['a', 'b', 'c'], "init")   # {'a': 'init', 'b': 'init', 'c': 'init'}

Reassigning is fine — d['a'] = 5 rebinds the key without affecting others, because integers are immutable. The shared-reference issue only manifests when the value is mutated in place.

d = dict.fromkeys(['a', 'b'], 0)
d['a'] = 5                               # OK — rebinds 'a'
print(d)                                 # {'a': 5, 'b': 0}

How to avoid

Dict comprehension with explicit creation

d = {k: [] for k in ['a', 'b', 'c']}
d['a'].append(1)
print(d)        # {'a': [1], 'b': [], 'c': []}

Each iteration creates a fresh []. No sharing.

defaultdict for the “compute on access” pattern

from collections import defaultdict
d = defaultdict(list)
d['a'].append(1)
d['b'].append(2)
print(dict(d))  # {'a': [1], 'b': [2]}

Factory called per access. Same fix as for the mutable default arguments gotcha.

Loop with explicit copy

template = {"items": [], "count": 0}
d = {k: dict(template) for k in keys}    # one fresh dict per key
# But values inside template are still shared if mutable! Need deepcopy:
import copy
d = {k: copy.deepcopy(template) for k in keys}

Real-world bug

# Initialize a per-user errors collector:
USERS = ['alice', 'bob', 'carol']
errors = dict.fromkeys(USERS, [])

errors['alice'].append("login failed")
print(errors)
# {'alice': ['login failed'], 'bob': ['login failed'], 'carol': ['login failed']}

Now every user has the same error. Bug.

Fix:

errors = {u: [] for u in USERS}

When dict.fromkeys is fine

When all keys legitimately should reference the same object (rare), or when the default is immutable:

# Counter initialization with 0 — fine:
seen = dict.fromkeys(items, 0)
for item in items:
    seen[item] += 1
# Setting all flags to the same reference (deliberately):
shared_state = SharedState()
flags = dict.fromkeys(['x', 'y', 'z'], shared_state)
# All three keys point to the same SharedState — intentional

A historical note: set.fromkeys doesn’t exist

set.fromkeys([1, 2, 3])     # AttributeError

Use a set literal or comprehension:

{1, 2, 3}
{x for x in range(3)}

Sets don’t have key/value pairs, so fromkeys makes no sense for them.

Interview angle

  • Q: “What does dict.fromkeys(['a', 'b', 'c'], []) produce, and what’s the trap?” — three keys sharing one list. Mutating one mutates all.
  • Q: “How do you fix it?” — dict comprehension with {k: [] for k in keys}, or defaultdict(list).
  • Follow-up: “Why does this happen?” — [] evaluates once; fromkeys binds same object to every key.
  • Follow-up: “Is this a problem with dict.fromkeys(keys, 0)?” — no, integers are immutable; reassignment doesn’t mutate.

See 01_mutable_default_arguments.md, 18_dataclass_mutable_default.md, stdlib/01_collections.md.