What is the best way to implement nested dictionaries?
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.
Introduction
Nested dictionaries in Python store hierarchical data as dictionaries within dictionaries. The best implementation depends on how deep and dynamic the nesting is. For fixed-depth nesting, collections.defaultdict with a factory function is the cleanest approach. For arbitrary-depth nesting, a recursive defaultdict (sometimes called an autovivifying dictionary) automatically creates intermediate levels. For typed data, dataclasses or TypedDict provide structure with IDE support and type checking.
The Problem with Plain Dicts
Method 1: defaultdict for Fixed-Depth Nesting
Each level requires wrapping defaultdict in another defaultdict. This works well for 2-3 levels but becomes unwieldy for deeper nesting.
Method 2: Recursive defaultdict (Autovivification)
This creates new defaultdict instances on the fly for any access. Convert to regular dicts for serialization:
Method 3: Custom NestedDict Class
__missing__ is called when a key is not found. This approach is cleaner than defaultdict because it subclasses dict directly and serializes naturally.
Method 4: Using setdefault for Safe Access
This is verbose but works without imports and uses plain dicts throughout.
Method 5: Dataclasses for Typed Nested Data
Dataclasses provide IDE autocompletion, type checking, and clear structure. Use them when the nested structure is known at development time.
Safe Deep Access with get()
Safe Deep Set
Merging Nested Dictionaries
Common Pitfalls
defaultdictcreates keys on read access: Accessingd["nonexistent"]silently creates that key with an empty default. This causes unexpected keys to appear. Usekey in dto check existence without side effects.- Sharing mutable defaults:
dict.fromkeys(["a", "b"], {})makes all keys share the same inner dict. Use{k: {} for k in keys}to create independent inner dicts. - JSON serialization of
defaultdict:json.dumps(defaultdict_obj)works becausedefaultdictsubclassesdict, but the default factory is lost on deserialization. Convert to regular dicts first if round-tripping matters. - No type safety with nested dicts: Typos in key names cause silent bugs (
data["usres"]creates a new key instead of raising an error). Use dataclasses or TypedDict for structured data where keys are known. - Recursive nesting with no depth limit: An autovivifying dict never raises KeyError, making bugs harder to catch. Consider adding a maximum depth or using a regular dict with explicit key checks for production data.
Summary
- Use
defaultdict(dict)ordefaultdict(lambda: defaultdict(list))for 2-3 levels of nesting - Use a recursive
defaultdictor customNestedDictclass for arbitrary-depth nesting - Use
dataclassesorTypedDictwhen the nested structure is known at development time - Use
deep_getanddeep_sethelpers for safe access to deeply nested plain dicts - Convert
defaultdictto regulardictbefore JSON serialization to avoid losing the factory function
Related reading
- What is the best way to modify a list in a 'foreach' loop?
- What is the best way to sort a partially ordered list?
- What is the best/preferred approach to implement Maximum Likelihood Estimation for large data sets in GBs
- What is the complexity of set_intersection in C?
- What is the best way to remove accents (normalize) in a Python unicode string?
- What is the best way to remove accents normalize in a Python unicode string?
- What is the correct usage of ConcurrentBag?
- What is the default initialization of an array in Java?

DSA Fundamentals
Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.