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.

Mental Model

Nested structures are containers inside containers, like folders inside folders. Each level of nesting requires an additional index to reach (matrix[row][col]). The critical pitfall is shared references: [[0]*3]*3 creates three references to the same inner list, while [[0]*3 for _ in range(3)] creates three independent lists.

Nested Lists

Lists containing other lists form multi-dimensional structures.

1. Creating 2D Lists

Build matrices and grids using nested lists.

```python

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.

```python 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.

```python

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.

```python

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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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.

```python 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] ```


Exercises

Exercise 1. Write a function flatten(nested) that takes an arbitrarily nested list and returns a flat list of all non-list elements. For example, flatten([1, [2, [3, 4]], 5]) should return [1, 2, 3, 4, 5].

Solution to Exercise 1
```python
def flatten(nested):
    result = []
    for item in nested:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result

print(flatten([1, [2, [3, 4]], 5]))  # [1, 2, 3, 4, 5]
print(flatten([[1, 2], [3, [4, [5]]]]))  # [1, 2, 3, 4, 5]
```

The function uses recursion to handle arbitrary nesting depth.


Exercise 2. Given the nested dictionary below, write a function safe_get(d, *keys, default=None) that navigates the nested structure safely. It should return the value if all keys exist, or default otherwise.

python data = {"user": {"address": {"city": "Seoul"}}}

Solution to Exercise 2
```python
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

data = {"user": {"address": {"city": "Seoul"}}}

print(safe_get(data, "user", "address", "city"))     # Seoul
print(safe_get(data, "user", "phone"))                # None
print(safe_get(data, "missing", "key", default="N/A"))  # N/A
```

The function chains .get() calls, returning the default if any key is missing.


Exercise 3. Explain the common pitfall of grid = [[0] * 3] * 3. What happens when you set grid[0][0] = 1? Write the correct way to create a 3x3 grid of zeros.

Solution to Exercise 3

[[0] * 3] * 3 creates three references to the same inner list. Modifying one row affects all rows:

```python
bad_grid = [[0] * 3] * 3
bad_grid[0][0] = 1
print(bad_grid)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

# Correct way
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]]
```

The list comprehension creates independent inner lists for each row.