nested defaultdict
python
collections module
python programming
data structures

Nested defaultdict of defaultdict

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Nested defaultdict objects are a compact way to build multi-level dictionaries in Python. They are especially useful for counting, grouping, and sparse matrix style data because they remove most of the repetitive “create this key if it does not exist yet” logic.

Why a Nested defaultdict Helps

With a regular dictionary, updating a two-level structure usually means several existence checks:

python
1counts = {}
2
3country = "us"
4device = "mobile"
5
6if country not in counts:
7    counts[country] = {}
8
9if device not in counts[country]:
10    counts[country][device] = 0
11
12counts[country][device] += 1

That is not hard to write, but it becomes noisy when repeated in many places. A nested defaultdict moves that setup logic into a factory so the update path stays clean.

Build a Two-Level Structure

The usual pattern is to make the outer defaultdict create inner defaultdict objects:

python
1from collections import defaultdict
2
3def make_inner():
4    return defaultdict(int)
5
6counts = defaultdict(make_inner)
7
8counts["us"]["mobile"] += 1
9counts["us"]["desktop"] += 2
10counts["ca"]["mobile"] += 3
11
12print(counts["us"]["mobile"])
13print(dict(counts["us"]))

Here, int is the leaf factory, so missing inner values start at 0. The named function is worth using because it makes the structure easier to read than a deeply nested lambda.

Using Lambdas for Short Cases

For quick scripts, a lambda can be fine:

python
1from collections import defaultdict
2
3matrix = defaultdict(lambda: defaultdict(float))
4
5matrix["row1"]["col1"] += 1.5
6matrix["row1"]["col2"] += 2.0
7
8print(matrix["row1"]["col1"])

This is concise, but once the depth increases, named factories are usually easier to maintain and debug.

A Real Grouping Example

Nested dictionaries are common in analytics and log processing. Suppose you want to count events by country and status:

python
1from collections import defaultdict
2
3def make_status_counts():
4    return defaultdict(int)
5
6events = [
7    ("us", "ok"),
8    ("us", "ok"),
9    ("us", "error"),
10    ("ca", "ok"),
11]
12
13stats = defaultdict(make_status_counts)
14
15for country, status in events:
16    stats[country][status] += 1
17
18for country, by_status in stats.items():
19    print(country, dict(by_status))

This avoids separate initialization code for every new country or status. It is a good fit when keys are discovered dynamically from input data.

Going Deeper Than Two Levels

Three-level nesting works the same way, but readability drops fast if the factories are not explicit:

python
1from collections import defaultdict
2
3def level3():
4    return defaultdict(int)
5
6def level2():
7    return defaultdict(level3)
8
9cube = defaultdict(level2)
10
11cube["x"]["y"]["z"] += 1
12print(cube["x"]["y"]["z"])

This is fine for sparse hierarchical data. If your shape is stable and well known, a class or dataclass is often clearer than many levels of dynamic dictionaries.

Convert to Plain dict Before Serialization

defaultdict works well in memory, but it is not always the best format for JSON output or public APIs. A recursive conversion step is a good habit:

python
1from collections import defaultdict
2
3def freeze(value):
4    if isinstance(value, defaultdict):
5        return {key: freeze(child) for key, child in value.items()}
6    return value
7
8data = defaultdict(lambda: defaultdict(int))
9data["team-a"]["passed"] += 4
10data["team-a"]["failed"] += 1
11
12plain = freeze(data)
13print(plain)

Converting before serialization also makes tests easier, because the snapshot output is a normal dictionary instead of an object with factory behavior.

Be Careful with Accidental Key Creation

The biggest tradeoff with defaultdict is that reads can mutate the structure. Accessing a missing key creates it immediately:

python
1from collections import defaultdict
2
3outer = defaultdict(lambda: defaultdict(int))
4print("before", dict(outer))
5
6_ = outer["missing"]["value"]
7
8print("after", {k: dict(v) for k, v in outer.items()})

If you only want to inspect data, use dict conversion plus .get() access on the frozen result. That avoids polluting the structure with empty branches.

Common Pitfalls

  • Using nested lambdas everywhere and ending up with a structure that is hard to understand later.
  • Forgetting that reading a missing key creates it, which can silently change program state.
  • Serializing a raw defaultdict directly instead of converting it to a plain dictionary first.
  • Building very deep nested structures when a dedicated class or dataclass would express the model more clearly.
  • Choosing the wrong leaf factory, such as list when the code expects numeric counters.

Summary

  • A nested defaultdict is useful when keys are discovered dynamically and missing branches should be created automatically.
  • Named factory functions usually make multi-level structures clearer than nested lambdas.
  • Use int, list, set, or another leaf factory that matches the actual data you store.
  • Convert nested defaultdict values to plain dictionaries before serialization or external output.
  • Be careful with accidental key creation during read-only lookups.

Related reading
Course
Intermediate
27 lessons
15 hours
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 course
Track 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.

Practice algorithms

All Rights Reserved.