Nested Dictionaries
Learn how to create, access, modify, delete, and iterate nested dictionaries in Python with clear examples and best practices.
A nested dictionary is a dictionary that contains other dictionaries as values. This creates a hierarchical (tree-like) data structure that is ideal for representing real-world grouped data — such as a collection of user profiles, a configuration file, or a JSON API response.
This chapter covers:
- Creating nested dictionaries
- Accessing values at any depth
- Modifying and adding entries
- Deleting keys and sub-dictionaries
- Iterating over nested dictionaries
- Using
.get()to avoidKeyError - Working with deeply-nested data and JSON
Creating Nested Dictionaries
Define a dictionary whose values are themselves dictionaries:
people = {
"person1": {"name": "Alice", "age": 30},
"person2": {"name": "Bob", "age": 25},
}
print(people)
# {'person1': {'name': 'Alice', 'age': 30}, 'person2': {'name': 'Bob', 'age': 25}}You can also build a nested dictionary incrementally:
people = {}
people["person1"] = {"name": "Alice", "age": 30}
people["person2"] = {"name": "Bob", "age": 25}
print(people["person1"]) # {'name': 'Alice', 'age': 30}There is no limit to the depth — an inner value can itself be a nested dictionary.
Accessing Nested Dictionary Values
Chain square-bracket lookups to drill into any level:
Safe Access with .get()
Chained bracket lookups raise a KeyError if a key is missing. Use .get() to return a default value instead:
people = {
"person1": {"name": "Alice", "age": 30},
}
# Safe: returns None if "person3" does not exist
print(people.get("person3", {}).get("name", "Unknown")) # Unknown
# Risky: raises KeyError
# print(people["person3"]["name"])The pattern .get(outer_key, {}).get(inner_key, default) is the idiomatic way to read optional nested data without a try/except block.
Modifying Nested Dictionary Values
Assign directly through the chain of keys:
Adding New Sub-Dictionaries
Assign a new dictionary literal to a new outer key:
Using setdefault() to Add Only When Missing
setdefault() inserts a key with a default value only if it does not already exist — useful when building nested dicts from a stream of data:
scores = {}
for student, subject, grade in [
("Alice", "math", 90),
("Alice", "english", 85),
("Bob", "math", 78),
]:
scores.setdefault(student, {})[subject] = grade
print(scores)
# {'Alice': {'math': 90, 'english': 85}, 'Bob': {'math': 78}}Deleting Keys and Sub-Dictionaries
Use del to remove a key (and its value) at any level:
people = {
"person1": {"name": "Alice", "age": 30},
"person2": {"name": "Bob", "age": 25},
"person3": {"name": "Carol", "age": 40},
}
del people["person2"]["age"] # remove one key from an inner dict
del people["person3"] # remove an entire sub-dictionary
print(people)
# {'person1': {'name': 'Alice', 'age': 30}, 'person2': {'name': 'Bob'}}Use .pop() if you also need the deleted value:
removed = people["person1"].pop("age", None)
print(removed) # 30
print(people["person1"]) # {'name': 'Alice'}Iterating Over Nested Dictionaries
Loop over outer keys and inner items
people = {
"person1": {"name": "Alice", "age": 30},
"person2": {"name": "Bob", "age": 25},
}
for person_id, details in people.items():
print(f"{person_id}:")
for key, value in details.items():
print(f" {key}: {value}")Output:
person1:
name: Alice
age: 30
person2:
name: Bob
age: 25Flatten a nested dict into a list of records
A common pattern when preparing data for processing:
people = {
"person1": {"name": "Alice", "age": 30},
"person2": {"name": "Bob", "age": 25},
}
records = [
{"id": pid, **info}
for pid, info in people.items()
]
print(records)
# [{'id': 'person1', 'name': 'Alice', 'age': 30},
# {'id': 'person2', 'name': 'Bob', 'age': 25}]Deeply Nested Dictionaries
Python imposes no limit on nesting depth. Here is a three-level example representing a company's department structure:
company = {
"engineering": {
"frontend": {
"lead": "Alice",
"headcount": 5,
},
"backend": {
"lead": "Bob",
"headcount": 8,
},
},
"marketing": {
"content": {
"lead": "Carol",
"headcount": 3,
},
},
}
# Access three levels deep
print(company["engineering"]["backend"]["lead"]) # Bob
# Iterate two levels and collect leads
leads = [
dept_data["lead"]
for dept_data in (
team
for teams in company.values()
for team in teams.values()
)
]
print(leads) # ['Alice', 'Bob', 'Carol']When reading deep keys that may be absent, chain .get() calls:
lead = company.get("hr", {}).get("recruitment", {}).get("lead", "Not assigned")
print(lead) # Not assignedNested Dictionaries and JSON
JSON objects map directly to Python nested dictionaries. The json module converts between them:
import json
data = {
"users": {
"u1": {"name": "Alice", "active": True},
"u2": {"name": "Bob", "active": False},
}
}
# Serialize to JSON string
json_str = json.dumps(data, indent=2)
print(json_str)
# Deserialize back to a nested dict
restored = json.loads(json_str)
print(restored["users"]["u1"]["name"]) # AliceThis is the typical workflow when consuming REST APIs or reading configuration files.
Common Gotchas
Shared reference trap. If you copy an inner dict by reference and then modify it, both the original and the copy change:
original = {"a": {"x": 1}}
shallow = original.copy() # copies only the outer dict
shallow["a"]["x"] = 99
print(original["a"]["x"]) # 99 ← original is affected!Use copy.deepcopy() when you need a fully independent copy:
import copy
original = {"a": {"x": 1}}
deep = copy.deepcopy(original)
deep["a"]["x"] = 99
print(original["a"]["x"]) # 1 ← original is safeSee the Copy Dictionaries chapter for a full comparison of shallow vs. deep copy.
KeyError on missing intermediate keys. Writing d["a"]["b"] = 1 raises KeyError if "a" does not exist yet. Use setdefault or collections.defaultdict to auto-create intermediate levels.
Quick Reference
| Task | Syntax |
|---|---|
| Access inner value | d["outer"]["inner"] |
| Safe access | d.get("outer", {}).get("inner", default) |
| Add / update inner key | d["outer"]["inner"] = value |
| Add new sub-dict | d["new_key"] = {...} |
| Delete inner key | del d["outer"]["inner"] |
| Delete sub-dict | del d["outer"] |
| Iterate all entries | for k, v in d.items(): for ik, iv in v.items(): |
| Deep copy | import copy; copy.deepcopy(d) |