Nested Dictionaries and Real Data Structures
Real data arrives as dictionaries inside dictionaries inside lists. Reaching into it safely, walking it and flattening it are skills worth practising deliberately.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
What nested data looks like
company = {
"name": "Lumen Works",
"founded": 2019,
"address": {
"city": "Pune",
"pin": "411001",
},
"departments": [
{
"name": "engineering",
"head": "Meera",
"staff": [
{"name": "Arun", "role": "backend", "years": 4},
{"name": "Sara", "role": "frontend", "years": 2},
],
},
{
"name": "design",
"head": "Ravi",
"staff": [
{"name": "Nita", "role": "ux", "years": 6},
],
},
],
}This is the shape configuration files, API responses and JSON documents arrive in. Dictionaries for records with named fields, lists for repeated items.
Reaching into it
print(company["name"]) # Lumen Works
print(company["address"]["city"]) # Pune
print(company["departments"][0]["name"]) # engineering
print(company["departments"][0]["staff"][1]["name"]) # SaraRead left to right, one step at a time. Each bracket resolves one level, and the type of what comes back decides whether the next bracket is a key or an index.
Reaching in safely
# Raises if any level is missing
# print(company["address"]["country"]) # KeyError
# Chained get with empty defaults
print(company.get("address", {}).get("country")) # None
print(company.get("owner", {}).get("name", "unknown")) # unknowndef dig(data, *keys, default=None):
"""Follow a path of keys or indexes, returning default if it breaks."""
current = data
for key in keys:
try:
current = current[key]
except (KeyError, IndexError, TypeError):
return default
return current
print(dig(company, "address", "city")) # Pune
print(dig(company, "address", "country", default="NA")) # NA
print(dig(company, "departments", 0, "staff", 1, "name")) # Sara
print(dig(company, "departments", 9, "name", default="-")) # -A helper like this pays for itself the first time you process an API response where half the optional fields are absent.
Walking it
for department in company["departments"]:
print(f"{department['name']} (head: {department['head']})")
for member in department["staff"]:
print(f" {member['name']:<8}{member['role']:<10}{member['years']} years")engineering (head: Meera)
Arun backend 4 years
Sara frontend 2 years
design (head: Ravi)
Nita ux 6 yearsCollecting across levels
everyone = [
member
for department in company["departments"]
for member in department["staff"]
]
print(len(everyone)) # 3
print([m["name"] for m in everyone]) # ['Arun', 'Sara', 'Nita']
print(sum(m["years"] for m in everyone)) # 12
print(max(everyone, key=lambda m: m["years"])["name"]) # Nitaby_role = {}
for department in company["departments"]:
for member in department["staff"]:
by_role.setdefault(member["role"], []).append(member["name"])
print(by_role) # {'backend': ['Arun'], 'frontend': ['Sara'], 'ux': ['Nita']}Building nested structures
from collections import defaultdict
sales = [
("north", "jan", 100),
("north", "feb", 150),
("south", "jan", 80),
]
# A dictionary whose default value is itself a counting dictionary
totals = defaultdict(lambda: defaultdict(int))
for region, month, amount in sales:
totals[region][month] += amount
print(totals["north"]["feb"]) # 150
print({r: dict(m) for r, m in totals.items()})Updating deeply
settings = {
"display": {"theme": "light", "size": 12},
"editor": {"tabs": 4},
}
overrides = {
"display": {"size": 14},
}
# A plain update REPLACES the whole nested dictionary
plain = {**settings, **overrides}
print(plain["display"]) # {'size': 14} - theme was lost
def deep_merge(base, extra):
"""Merge extra into a copy of base, recursing into nested dictionaries."""
result = dict(base)
for key, value in extra.items():
if isinstance(result.get(key), dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
print(deep_merge(settings, overrides)["display"])
# {'theme': 'light', 'size': 14}Flattening
def flatten(data, prefix=""):
"""Turn nested dictionaries into one level with dotted keys."""
flat = {}
for key, value in data.items():
path = f"{prefix}{key}"
if isinstance(value, dict):
flat.update(flatten(value, prefix=path + "."))
else:
flat[path] = value
return flat
config = {"db": {"host": "localhost", "port": 5432}, "debug": True}
print(flatten(config))
# {'db.host': 'localhost', 'db.port': 5432, 'debug': True}Walking to any depth
def walk(data, path=()):
"""Yield (path, value) for every leaf in a nested structure."""
if isinstance(data, dict):
for key, value in data.items():
yield from walk(value, path + (key,))
elif isinstance(data, list):
for index, value in enumerate(data):
yield from walk(value, path + (index,))
else:
yield path, data
for path, value in walk({"a": {"b": [1, 2]}}):
print(path, "=", value)
# ('a', 'b', 0) = 1
# ('a', 'b', 1) = 2When to stop using dictionaries
from dataclasses import dataclass
@dataclass
class StaffMember:
name: str
role: str
years: int
team = [
StaffMember("Arun", "backend", 4),
StaffMember("Sara", "frontend", 2),
]
print(team[0].name) # attribute access
print(sum(m.years for m in team)) # 6
# print(team[0].yeras) # AttributeError, caught immediatelyA dictionary accepts any key, so member["yeras"] fails only at the moment it runs, and a typed misspelling in an assignment silently creates a new field. Once a structure has a fixed shape that your own code creates, a dataclass gives you attribute access, a readable repr and real errors. Keep dictionaries for data that arrives from outside.
Common mistakes
- Chaining
[]through levels that may be absent. - Using
{**a, **b}on nested data and losing whole sub-dictionaries. - Misspelling a key on assignment, silently creating a new one.
- Mixing keys and indexes in the wrong order when the level is a list.
- Mutating a shared nested structure through a shallow copy.
- Nesting five levels deep when a class or a flat key would be clearer.
Best practices
- Write one small helper for safe deep access and reuse it.
- Use nested comprehensions to flatten before analysing.
- Use
defaultdictfor building nested aggregations. - Deep merge deliberately; the built in merge is shallow.
- Convert external data into dataclasses at the boundary once its shape is known.
Practice
- Print every staff member across all departments with their department name.
- Write a function returning the department with the highest total years of experience.
- Implement a safe deep get that accepts a dotted string path such as
"address.city". - Deep merge two configuration dictionaries three levels deep and verify nothing is lost.
- Flatten a nested structure to dotted keys, then write the function that reverses it.
Conclusion
Nested data is dictionaries for records and lists for repetition. Access it one level at a time, guard every optional level, use comprehensions to flatten it before analysing, and convert it to classes once its shape is settled.