W3docs

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 avoid KeyError
  • 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:

python— editable, runs on the server

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:

python— editable, runs on the server

Adding New Sub-Dictionaries

Assign a new dictionary literal to a new outer key:

python— editable, runs on the server

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: 25

Flatten 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 assigned

Nested 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"])  # Alice

This 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 safe

See 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

TaskSyntax
Access inner valued["outer"]["inner"]
Safe accessd.get("outer", {}).get("inner", default)
Add / update inner keyd["outer"]["inner"] = value
Add new sub-dictd["new_key"] = {...}
Delete inner keydel d["outer"]["inner"]
Delete sub-dictdel d["outer"]
Iterate all entriesfor k, v in d.items(): for ik, iv in v.items():
Deep copyimport copy; copy.deepcopy(d)

Practice

Practice
Which method safely reads a missing key from a nested dict without raising a KeyError?
Which method safely reads a missing key from a nested dict without raising a KeyError?
Was this page helpful?