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.

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
| Need | Better choice |
|---|---|
| Count occurrences | collections.Counter (cleaner API) |
| Strict schema | TypedDict or Pydantic |
| JSON-friendly storage | Plain dict + setdefault() |
| Group by key | itertools.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.
