Module 3 · Control Flow & Logic ⏱ 19 min

Nested Data & Loops

By the end of this lesson you will be able to:
  • Access items in a nested list (matrix) using double indexing
  • Write a nested loop to visit every element of a matrix
  • Iterate over a list of dictionaries and extract values by key
  • Handle jagged rows safely without IndexError

Real data is rarely flat. A spreadsheet is a list of rows, where each row is a list of cells. An API response is often a list of dictionaries. These are nested structures — containers inside containers.

A matrix is just a list of lists:

matrix = [
    [1, 2, 3],
    [4, 5, 6]
]
print(matrix[0][1])  # 2 — row 0, column 1

To visit every element, you use a loop inside a loop. But before you can loop, you need to understand how Python reaches into layers of nesting. That skill — double indexing — is the first bridge from toy programs to real data processing.

flowchart TD
  M["matrix"] --> R0["row 0"]
  M --> R1["row 1"]
  R0 --> E0["col 0"]
  R0 --> E1["col 1"]
  R1 --> E2["col 0"]
  R1 --> E3["col 1"]
  style M fill:#3776ab,color:#fff
A matrix is a list of rows; each row is a list of elements.

Reaching inside: double indexing

matrix[0] returns the first row, which is itself a list. matrix[0][1] takes that list and returns the item at index 1. You can chain as many brackets as you have layers.

data = [
    [{'id': 1}, {'id': 2}],
    [{'id': 3}, {'id': 4}]
]
print(data[1][0]['id'])  # 3

The rule is simple: work from left to right, one bracket at a time. Each bracket peels off one layer. If you miscount, you get an IndexError — the most common nested-data bug. Always ask: what type does this bracket return, and is the next bracket legal on that type?

Double indexing reaches into rows and columns.
matrix = [
    [1, 2, 3],
    [4, 5, 6]
]
print(matrix[0][1])
print(matrix[1][2])
print(matrix[1][0])

Looping through every element

The outer loop walks through rows; the inner loop walks through the items in that row. For each iteration of the outer loop, the inner loop runs to completion.

for row in matrix:
    for val in row:
        print(val)

This pattern scales to any depth: three nested lists need three nested loops. But depth has a cost — each extra loop makes the code harder to read. As a rule, if you need more than two levels of nesting, consider flattening the data first or using helper functions.

You can do the same with a list of dictionaries: loop over the list, then read each dictionary by its key. The shape changes but the pattern stays the same.

Nested loops sum a matrix and read dict keys from a list.
matrix = [[1, 2], [3, 4], [5, 6]]
total = 0
for row in matrix:
    for val in row:
        total += val
print('sum:', total)

people = [
    {"name": "Ada", "age": 28},
    {"name": "Bob", "age": 24}
]
for person in people:
    print(person["name"], "is", person["age"])

Aggregation patterns: sum, count, and find-max

Nested loops usually do more than print. They aggregate — sum all values, count matching items, or find the maximum. The pattern is always the same: initialize an accumulator before the loops, update it inside the inner loop, and return it after.

total = 0
for row in matrix:
    for val in row:
        total += val

Finding the maximum is nearly identical, but you initialize with None or the first element and use > instead of +. Recognizing these patterns lets you read nested-loop code quickly, because the scaffolding stays the same even when the data changes.

flowchart TD
  L["people list"] --> D1["{name:'Ada',age:28}"]
  L --> D2["{name:'Bob',age:24}"]
  D1 --> K1["name: 'Ada'"]
  D1 --> K2["age: 28"]
  style L fill:#3776ab,color:#fff
A list of dictionaries is the standard shape for records from an API.

Lists of dictionaries: the API shape

Real APIs rarely return raw matrices. They return a list of objects, and in Python those objects are dictionaries. Each dict has the same keys, so you can process them uniformly.

people = [
    {"name": "Ada", "age": 28, "city": "London"},
    {"name": "Bob", "age": 24, "city": "Paris"}
]

To find the average age, you loop over the list, read person["age"] each time, and accumulate a total. The key lookup is constant-time, so this scales well even with thousands of records. The trick is remembering that person is a dict, not a number — person[0] would be wrong.

Aggregate over a list of dictionaries to compute averages and counts.
people = [
    {"name": "Ada", "age": 28, "city": "London"},
    {"name": "Bob", "age": 24, "city": "Paris"},
    {"name": "Cho", "age": 32, "city": "London"}
]

total = 0
for person in people:
    total += person["age"]
print('average age:', total // len(people))

londoners = 0
for person in people:
    if person["city"] == "London":
        londoners += 1
print('London count:', londoners)

Jagged lists: when rows have different lengths

Not every nested list is a tidy rectangle. A jagged list has rows of different lengths:

scores = [
    [95, 88],
    [72],
    [91, 85, 76, 80]
]

If you assume every row has two elements and write row[1], the second row crashes with an IndexError. The safe pattern is to loop over the row itself — for score in row: — rather than indexing by position. When you truly need an index, guard it with len(row).

Jagged data appears in log files, user-generated content, and scraped HTML. Treating it as rectangular is one of the most common sources of production crashes in data-processing scripts.

flowchart TD
  R["rectangular"] --> R0["[1, 2, 3]"]
  R --> R1["[4, 5, 6]"]
  J["jagged"] --> J0["[1, 2]"]
  J --> J1["[3]"]
  J --> J2["[4, 5, 6, 7]"]
  style R fill:#3776ab,color:#fff
  style J fill:#b45309,color:#fff
A rectangular matrix has uniform rows; a jagged list does not.
Iterate jagged rows safely by checking length or looping directly.
scores = [
    [95, 88],
    [72],
    [91, 85, 76]
]
for row in scores:
    print('row has', len(row), 'items')
    for score in row:
        print('  ', score)

print('---')
for row in scores:
    if len(row) > 1:
        print('second item:', row[1])
Exercise

Write sum_matrix(matrix) that returns the sum of all numbers in a nested list (a matrix).

def sum_matrix(matrix):
    total = 0
    # loop through rows, then values
    return total
Exercise

What does this print? The loop reads the name key from each dictionary.

people = [
    {"name": "Ada", "age": 28},
    {"name": "Bob", "age": 24}
]
for person in people:
    print(person["name"])
Exercise

Write average_per_row(matrix) that returns a list of the average of each row, using integer division //. For an empty matrix, return []. For a row like [1, 2, 3], the average is 2.

def average_per_row(matrix):
    # return a list of averages, one per row
    pass
Exercise

This function is meant to collect the second element of every row, but it crashes on jagged data. Fix it to skip rows that don't have a second element.

def second_elements(matrix):
    result = []
    for row in matrix:
        result.append(row[1])
    return result
Exercise

Write find_oldest(people) that takes a list of dictionaries like {'name': 'Ada', 'age': 28} and returns the name of the oldest person. If there is a tie, return the first one. Return None for an empty list.

def find_oldest(people):
    # return the name of the oldest person
    pass

Recap

  • A matrix is a list of lists; access items with double indexing matrix[row][col].
  • A nested loop visits every element by iterating rows, then items in each row.
  • A list of dictionaries is the standard shape for real records; loop the list and read by key.
  • Jagged rows have different lengths; loop the row or check len(row) before indexing.
  • When nesting gets deeper than two levels, consider flattening or helper functions.

Next you will build a complete text-analysis tool that combines functions, dictionaries, and loops into a real project.

Checkpoint quiz

How do you access the element at row 1, column 2 in matrix = [[1, 2, 3], [4, 5, 6]]?

What does a nested loop over a matrix do?

Go deeper — technical resources