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
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?
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.
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
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.
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
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])
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
Use an outer loop for rows and an inner loop for values in each row.
Add every
valtototal.
def sum_matrix(matrix):
total = 0
for row in matrix:
for val in row:
total += val
return total
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"])
The loop runs once per dictionary.
Each iteration prints the value stored under the key 'name'.
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
Loop over each row, compute sum(row) // len(row), and append.
Skip empty rows or your division will crash.
def average_per_row(matrix):
result = []
for row in matrix:
if row:
result.append(sum(row) // len(row))
return result
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
Check
len(row)before indexing with[1].Only append when the row has at least 2 elements.
def second_elements(matrix):
result = []
for row in matrix:
if len(row) > 1:
result.append(row[1])
return result
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
Start with the first person as the current oldest.
Update only when a strictly greater age is found, so ties keep the first.
def find_oldest(people):
if not people:
return None
oldest = people[0]
for person in people:
if person['age'] > oldest['age']:
oldest = person
return oldest['name']
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.