Skip to content

Nested Structures

Composite data types can contain other composite types, creating hierarchical data structures. Understanding nested structures is essential for working with real-world data.

Nested Lists

Lists containing other lists form multi-dimensional structures.

1. Creating 2D Lists

Build matrices and grids using nested lists.

# Direct creation
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Using comprehension
rows, cols = 3, 4
grid = [[0 for _ in range(cols)] for _ in range(rows)]
print(grid)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

2. Accessing Elements

Use multiple indices to access nested elements.

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Single element
print(matrix[0][0])    # 1
print(matrix[1][2])    # 6

# Entire row
print(matrix[0])       # [1, 2, 3]

# Column (requires iteration)
col = [row[1] for row in matrix]
print(col)             # [2, 5, 8]

3. Common Pitfall

Avoid creating shared references when initializing.

# Wrong: all rows share same list
bad_grid = [[0] * 3] * 3
bad_grid[0][0] = 1
print(bad_grid)
# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]  # All changed!

# Correct: independent lists
good_grid = [[0] * 3 for _ in range(3)]
good_grid[0][0] = 1
print(good_grid)
# [[1, 0, 0], [0, 0, 0], [0, 0, 0]]  # Only first changed

Nested Dicts

Dictionaries containing dictionaries model hierarchical data.

1. Creating Hierarchy

Build tree-like structures with nested dicts.

# User profile
user = {
    "name": "Alice",
    "contact": {
        "email": "alice@example.com",
        "phone": "123-456-7890"
    },
    "address": {
        "city": "Seoul",
        "country": "Korea"
    }
}

2. Accessing Nested Values

Chain keys to access deeply nested data.

user = {
    "name": "Bob",
    "settings": {
        "theme": "dark",
        "notifications": {
            "email": True,
            "sms": False
        }
    }
}

# Direct access
print(user["settings"]["theme"])              # dark
print(user["settings"]["notifications"]["email"])  # True

# Safe access with get()
print(user.get("settings", {}).get("language", "en"))  # en

3. Modifying Nested Data

Update values at any level of nesting.

config = {
    "database": {
        "host": "localhost",
        "port": 5432
    }
}

# Update existing
config["database"]["port"] = 3306

# Add new nested key
config["database"]["user"] = "admin"

# Add new section
config["logging"] = {"level": "INFO"}

print(config)
# {'database': {'host': 'localhost', 'port': 3306, 'user': 'admin'},
#  'logging': {'level': 'INFO'}}

Mixed Structures

Combine lists and dicts for flexible data modeling.

1. List of Dicts

Common pattern for collections of records.

students = [
    {"name": "Alice", "score": 95},
    {"name": "Bob", "score": 87},
    {"name": "Carol", "score": 92}
]

# Access by index then key
print(students[0]["name"])  # Alice

# Iterate records
for s in students:
    print(f"{s['name']}: {s['score']}")

2. Dict of Lists

Store multiple values per key.

grades = {
    "Alice": [95, 88, 92],
    "Bob": [87, 91, 85],
    "Carol": [92, 94, 90]
}

# Access all grades for one student
print(grades["Alice"])         # [95, 88, 92]

# Calculate averages
averages = {name: sum(g)/len(g) for name, g in grades.items()}
print(averages)
# {'Alice': 91.67, 'Bob': 87.67, 'Carol': 92.0}

3. Complex Hierarchies

Model real-world hierarchical data.

company = {
    "name": "TechCorp",
    "departments": [
        {
            "name": "Engineering",
            "employees": [
                {"name": "Alice", "role": "Lead"},
                {"name": "Bob", "role": "Developer"}
            ]
        },
        {
            "name": "Sales",
            "employees": [
                {"name": "Carol", "role": "Manager"}
            ]
        }
    ]
}

# Navigate the hierarchy
eng = company["departments"][0]
lead = eng["employees"][0]["name"]
print(lead)  # Alice

Iteration Patterns

Common patterns for traversing nested structures.

1. Nested Loops

Use nested loops for multi-level iteration.

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Iterate all elements
for row in matrix:
    for val in row:
        print(val, end=" ")
    print()
# 1 2 3
# 4 5 6
# 7 8 9

# With indices
for i, row in enumerate(matrix):
    for j, val in enumerate(row):
        print(f"[{i},{j}]={val}", end=" ")

2. Recursive Traversal

Handle arbitrary nesting depth with recursion.

def flatten(nested):
    """Flatten arbitrarily nested lists."""
    result = []
    for item in nested:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result

data = [1, [2, 3], [4, [5, 6]], 7]
print(flatten(data))  # [1, 2, 3, 4, 5, 6, 7]

3. Dict Traversal

Recursively process nested dictionaries.

def find_all_values(d, target_key):
    """Find all values for a key at any nesting level."""
    results = []
    for key, value in d.items():
        if key == target_key:
            results.append(value)
        if isinstance(value, dict):
            results.extend(find_all_values(value, target_key))
        elif isinstance(value, list):
            for item in value:
                if isinstance(item, dict):
                    results.extend(find_all_values(item, target_key))
    return results

data = {
    "name": "Root",
    "children": [
        {"name": "Child1"},
        {"name": "Child2", "children": [{"name": "Grandchild"}]}
    ]
}
print(find_all_values(data, "name"))
# ['Root', 'Child1', 'Child2', 'Grandchild']

Safe Access

Handle missing keys and indices gracefully.

1. Using get() Method

Provide defaults for missing dictionary keys.

data = {"user": {"name": "Alice"}}

# Unsafe - raises KeyError
# print(data["user"]["email"])

# Safe with get()
email = data.get("user", {}).get("email", "N/A")
print(email)  # N/A

# Nested get pattern
def safe_get(d, *keys, default=None):
    for key in keys:
        if isinstance(d, dict):
            d = d.get(key, default)
        else:
            return default
    return d

print(safe_get(data, "user", "name"))     # Alice
print(safe_get(data, "user", "email"))    # None

2. Try-Except Pattern

Catch access errors for mixed structures.

def safe_access(data, path):
    """Safely access nested data using path list."""
    try:
        result = data
        for key in path:
            result = result[key]
        return result
    except (KeyError, IndexError, TypeError):
        return None

data = {"items": [{"id": 1}, {"id": 2}]}

print(safe_access(data, ["items", 0, "id"]))   # 1
print(safe_access(data, ["items", 5, "id"]))   # None
print(safe_access(data, ["missing", "key"]))   # None

3. Default Factories

Use defaultdict for auto-creating nested levels.

from collections import defaultdict

# Auto-create nested dicts
def nested_dict():
    return defaultdict(nested_dict)

data = nested_dict()
data["a"]["b"]["c"] = 1
print(data["a"]["b"]["c"])  # 1

# Auto-create lists
groups = defaultdict(list)
items = [("a", 1), ("b", 2), ("a", 3)]
for key, val in items:
    groups[key].append(val)
print(dict(groups))  # {'a': [1, 3], 'b': [2]}

Copy Strategies

Create independent copies of nested structures.

1. Shallow vs Deep

Understand the difference between copy types.

import copy

original = [[1, 2], [3, 4]]

# Shallow copy - nested lists are shared
shallow = original.copy()
shallow[0][0] = 99
print(original[0][0])  # 99 (changed!)

# Deep copy - fully independent
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original[0][0])  # 1 (unchanged)

2. When to Use Each

Choose copy strategy based on mutation needs.

import copy

# Shallow: when nested data won't be modified
config_template = {"name": "default", "options": [1, 2, 3]}
config = config_template.copy()
config["name"] = "custom"  # Safe, strings are immutable

# Deep: when nested data will be modified
config = copy.deepcopy(config_template)
config["options"].append(4)  # Safe, independent list

3. JSON Round-Trip

Alternative deep copy for JSON-serializable data.

import json

original = {"nested": {"value": [1, 2, 3]}}

# JSON serialization creates deep copy
copied = json.loads(json.dumps(original))
copied["nested"]["value"].append(4)

print(original["nested"]["value"])  # [1, 2, 3]
print(copied["nested"]["value"])    # [1, 2, 3, 4]