Python KeyError with defaultdict: Why It Still Happens (2026)

You picked collections.defaultdict specifically to avoid KeyError, then it still raised one. The most common 2026 causes: you used .get() or **unpacking (which bypasses the default factory), you serialized it to JSON and back (loses the factory), or you passed the wrong callable to the constructor.

Python KeyError with defaultdict Why It Still Happens (2026)

The minimal reproducer

from collections import defaultdict
counts = defaultdict(int)
counts['apples'] += 1  # works, returns 1

# Now from JSON
import json
data = json.loads(json.dumps(counts))
data['oranges'] += 1  # TypeError or KeyError, defaultdict-ness was lost

Cause 1: .get() bypasses the default factory

d = defaultdict(list)
d['a'].append(1)        # default factory runs: d['a'] = []
val = d['missing']      # default factory runs: d['missing'] = []
val2 = d.get('also_missing')  # returns None, no factory call

# .get() is just dict.get(), it doesn't know about default_factory

If you want both behaviors, call the key with bracket access first or pass an explicit default to .get().

Cause 2: Serialization drops the factory

# Wrong: defaultdict becomes plain dict after JSON roundtrip
import json
counts = defaultdict(int)
counts['apples'] = 5
serialized = json.dumps(counts)
restored = json.loads(serialized)  # restored is a plain dict
restored['oranges'] += 1  # KeyError

# Right: wrap in defaultdict after restore
restored = defaultdict(int, json.loads(serialized))
restored['oranges'] += 1  # works, returns 1

Cause 3: Pickle preserves the factory if you pickle correctly

import pickle
counts = defaultdict(int)
counts['apples'] = 5

# Pickle preserves defaultdict-ness AND the factory
restored = pickle.loads(pickle.dumps(counts))
restored['oranges'] += 1  # works, factory survives

Cause 4: Wrong callable in constructor

# Wrong: passing an instance instead of a callable
d = defaultdict([])  # TypeError: first argument must be callable

# Right: pass the type or a lambda
d = defaultdict(list)        # callable: list, calls list() = []
d = defaultdict(lambda: 'N/A')
d = defaultdict(dict)        # for nested defaultdict

When NOT to use defaultdict

NeedBetter choice
Count occurrencescollections.Counter (cleaner API)
Strict schemaTypedDict or Pydantic
JSON-friendly storagePlain dict + setdefault()
Group by keyitertools.groupby or defaultdict(list)

Frequently Asked Questions

Why does defaultdict.get() return None instead of the default?

defaultdict inherits .get() from dict, which does not call the default_factory. Bracket access d[key] is the only operation that triggers the factory. To get default-aware get, use d[key] or d.setdefault(key, factory()).

How do I serialize a defaultdict to JSON without losing the factory?

JSON cannot represent the factory. Use json.dumps(dict(my_defaultdict)) to serialize, then re-wrap on load: defaultdict(int, json.loads(text)). For full round-trip preservation, use pickle instead of JSON.

Is defaultdict thread-safe?

No. defaultdict has the same thread-safety properties as dict: individual operations are atomic under GIL but compound operations (check-then-update) are not. For multi-threaded counting, use threading.Lock or use a thread-local Counter.

Should I use defaultdict or Counter for counting?

Counter for pure counting (it has Counter() + Counter() merging, most_common(), and elements()). defaultdict(int) when you only need basic incrementing or you also store other types of values. Counter is a subclass of dict, so both work in most contexts.

Can I nest defaultdict for 2-level grouping?

Yes. Use a lambda or partial: nested = defaultdict(lambda: defaultdict(list)). Now nested[‘key1’][‘key2’].append(‘item’) works without any pre-initialization. Be careful with serialization: lambdas cannot be pickled.

Leave a Comment