How to Create Multidimensional Nested Dictionaries in Python
Nested dictionaries are one of the most practical tools in Python for representing hierarchical data. Whether you are building configuration systems, modeling tree structures, or organizing multi-level mappings, knowing how to create and manage nested dictionaries effectively is essential. Without the right approach, you will quickly run into KeyError exceptions and verbose boilerplate code.
This guide covers several techniques for creating multidimensional nested dictionaries in Python, from recursive defaultdict patterns to helper functions, so you can pick the best approach for your use case.
Using defaultdict with Autovivification
The most flexible and elegant approach uses a recursive defaultdict that automatically creates new nested levels whenever you access a missing key. This pattern is known as autovivification.
from collections import defaultdict
def tree():
return defaultdict(tree)
data = tree()
data['Europe']['France']['Paris'] = 'Eiffel Tower'
data['Asia']['Japan']['Tokyo'] = 'Skytree'
print(data['Europe']['France']['Paris'])
Output:
Eiffel Tower
No KeyError, no manual initialization of intermediate dictionaries. Every missing key along the path is created automatically as a new defaultdict.
How Autovivification Works
Each time you access a key that does not exist, the defaultdict calls the tree factory function, which returns another defaultdict(tree). This creates an arbitrarily deep chain of nested dictionaries on the fly:
from collections import defaultdict
def tree():
return defaultdict(tree)
data = tree()
# No KeyError: each level is created automatically
data['level1']['level2']['level3']['level4'] = 'deep value'
# Accessing non-existent paths creates empty branches
print(type(data['new']['path']))
Output:
<class 'collections.defaultdict'>
The term "autovivification" comes from Perl and means "automatic creation of nested structures." This pattern eliminates the need to check whether intermediate keys exist before assigning values.
Converting to a Regular Dictionary
Autovivified structures are defaultdict instances, which can cause unexpected behavior during serialization or debugging. To get a clean, standard dictionary, convert recursively:
from collections import defaultdict
import json
def tree():
return defaultdict(tree)
def to_regular_dict(d):
"""Recursively convert defaultdict to regular dict."""
if isinstance(d, defaultdict):
return {k: to_regular_dict(v) for k, v in d.items()}
return d
data = tree()
data['users']['alice']['settings']['theme'] = 'dark'
data['users']['alice']['settings']['language'] = 'en'
regular = to_regular_dict(data)
print(json.dumps(regular, indent=2))
Output:
{
"users": {
"alice": {
"settings": {
"theme": "dark",
"language": "en"
}
}
}
}
This conversion is especially important when you need to pass the dictionary to functions that expect a plain dict, such as json.dumps or third-party APIs.
Using setdefault for Simple Nesting
Python's built-in setdefault method provides a straightforward way to build nested dictionaries without importing anything. It returns the value for a key if it exists, or sets it to a default and returns that default otherwise.
data = {}
data.setdefault('users', {})['alice'] = {'age': 25, 'role': 'admin'}
data.setdefault('users', {})['bob'] = {'age': 30, 'role': 'user'}
print(data)
Output:
{'users': {'alice': {'age': 25, 'role': 'admin'}, 'bob': {'age': 30, 'role': 'user'}}}
Chaining setdefault Calls
For deeper nesting, you can chain multiple setdefault calls together:
data = {}
data.setdefault('config', {}).setdefault('database', {})['host'] = 'localhost'
data.setdefault('config', {}).setdefault('database', {})['port'] = 5432
print(data)
Output:
{'config': {'database': {'host': 'localhost', 'port': 5432}}}
While setdefault is built-in and requires no imports, it becomes verbose quickly for deeply nested structures. If your nesting exceeds two or three levels, consider switching to defaultdict or a helper function.
Using Dictionary Comprehensions
When the structure of your nested dictionary is known in advance, dictionary comprehensions offer a clean and readable way to initialize everything at once.
# Create a 3x3 grid initialized to zero
grid = {x: {y: 0 for y in range(3)} for x in range(3)}
print(grid)
Output:
{0: {0: 0, 1: 0, 2: 0}, 1: {0: 0, 1: 0, 2: 0}, 2: {0: 0, 1: 0, 2: 0}}
You can then access and modify individual cells directly:
grid[1][1] = 5
print(grid[1][1])
Output:
5
Creating Templates with Comprehensions
Comprehensions are particularly useful for generating structured templates for multiple entities:
users = ['alice', 'bob', 'charlie']
user_data = {
user: {
'profile': {'name': user.title(), 'bio': ''},
'settings': {'notifications': True, 'theme': 'light'},
'stats': {'posts': 0, 'followers': 0}
}
for user in users
}
print(user_data['alice']['settings']['theme'])
Output:
light
This approach is ideal when every entry shares the same shape and you want to avoid repetitive manual initialization.
Creating Helper Functions for Controlled Access
If you want the convenience of automatic key creation without the side effects of autovivification, helper functions give you precise control:
def set_nested(dictionary, keys, value):
"""Set a value in a nested dictionary, creating intermediate dicts as needed."""
for key in keys[:-1]:
dictionary = dictionary.setdefault(key, {})
dictionary[keys[-1]] = value
def get_nested(dictionary, keys, default=None):
"""Safely get a value from a nested dictionary."""
for key in keys:
if isinstance(dictionary, dict):
dictionary = dictionary.get(key, default)
else:
return default
return dictionary
# Usage
data = {}
set_nested(data, ['a', 'b', 'c', 'd'], 'deep value')
print(data)
print(get_nested(data, ['a', 'b', 'c', 'd']))
print(get_nested(data, ['x', 'y', 'z'], 'not found'))
Output:
{'a': {'b': {'c': {'d': 'deep value'}}}}
deep value
not found
This pattern keeps the underlying dictionary as a plain dict, avoids creating keys on read access, and provides a safe fallback for missing paths.
Practical Examples
Configuration Management
Autovivification is a natural fit for building configuration objects where the full key hierarchy is not known upfront:
from collections import defaultdict
def tree():
return defaultdict(tree)
config = tree()
config['app']['server']['host'] = '0.0.0.0'
config['app']['server']['port'] = 8080
config['app']['database']['primary']['host'] = 'db1.example.com'
config['app']['database']['replica']['host'] = 'db2.example.com'
config['app']['features']['dark_mode'] = True
Grouping Data Hierarchically
Nested dictionaries are perfect for organizing data along multiple dimensions, such as region, year, and quarter:
from collections import defaultdict
def tree():
return defaultdict(tree)
sales = tree()
sales['North America']['2024']['Q1'] = 150000
sales['North America']['2024']['Q2'] = 175000
sales['Europe']['2024']['Q1'] = 120000
sales['Asia']['2023']['Q4'] = 200000
Common Pitfall: Autovivification Creates Keys on Read
One important behavior to be aware of when using defaultdict autovivification is that reading a missing key creates it. This can lead to subtle bugs.
Wrong approach (unintentional key creation):
from collections import defaultdict
def tree():
return defaultdict(tree)
data = tree()
data['users']['alice'] = 'admin'
# This check accidentally creates the key 'bob'
if data['users']['bob']:
print("Bob exists")
print(list(data['users'].keys()))
Output:
['alice', 'bob']
The key bob now exists even though you only intended to check for it.
Correct approach (use in for existence checks):
from collections import defaultdict
def tree():
return defaultdict(tree)
data = tree()
data['users']['alice'] = 'admin'
# Safe check: does not create the key
if 'bob' in data['users']:
print("Bob exists")
else:
print("Bob does not exist")
print(list(data['users'].keys()))
Output:
Bob does not exist
['alice']
Always use if 'key' in data instead of if data['key'] when working with defaultdict autovivification. The latter silently creates the key as a side effect.
Method Comparison
| Method | Best For | Auto-Creates Keys | Complexity |
|---|---|---|---|
defaultdict(tree) | Deep, unpredictable nesting | Yes, all levels | Low |
setdefault | Simple one or two level structures | Yes, one level at a time | Low |
| Dictionary comprehension | Fixed, known structures | No | Low |
| Helper functions | Controlled access patterns | Configurable | Medium |
Summary
Choose defaultdict(tree) when you need flexible, deeply nested structures and the hierarchy is not known in advance.
- Use
setdefaultfor simpler cases with predictable and shallow depth. Dictionary comprehensions are best when you need to initialize a fixed template structure upfront. - For maximum control without side effects, helper functions like
set_nestedandget_nestedoffer a clean middle ground.
By understanding the trade-offs of each approach, you can avoid common pitfalls like unintentional key creation and write cleaner, more maintainable Python code for any hierarchical data scenario.