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.

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

Read 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"))      # unknown
def 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 years

Collecting 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"])     # Nita
by_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) = 2

When 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 immediately

A 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 defaultdict for 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

  1. Print every staff member across all departments with their department name.
  2. Write a function returning the department with the highest total years of experience.
  3. Implement a safe deep get that accepts a dotted string path such as "address.city".
  4. Deep merge two configuration dictionaries three levels deep and verify nothing is lost.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Sorting Algorithms

Python sorts for you in n log n. Implementing bubble, insertion, merge and quick sort is still worth doing, because it teaches how algorithms are comp...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.