numbers = [ [1, 2, 3, 5, 6], [7, 8, 9, 10, 11], [12, 13, 14, 15, 16], [17, 18, 19, 20, 21], ] # Entire nested list print(numbers) # Third row of values print(numbers[2]) # First row, second column's value print(numbers[0][1])
A common pattern is to store data, especially numeric data, in a list of lists. This structure is like the tables that we saw with nesting dataclasses in lists. Except now, every item inside the table cells must be the same type. Structurally, the inner lists all have the same element type: a primitive type. The outer list has a somewhat complex element type: a list of primitives. That means the type of the entire outer value is a list of lists of primitives. Folks sometimes refer to these as a “matrix,” a “grid,” or a “2-dimensional” list of numbers. The representation is very useful for representing X- and Y-coordinate data.
numbers = [
[1, 2, 3, 5, 6],
[7, 8, 9, 10, 11],
[12, 13, 14, 15, 16],
[17, 18, 19, 20, 21],
]
# Iterate through outer list
for row in numbers:
# Iterate through inner list
for number in row:
# Reached actual integer
print(number)
Frequently, you will want to loop over the data, just like we did with one-dimensional lists.
To do so, we need to nest the for loops as shown.
In this example, notice how when we iterate through the outermost list, we get a row for each element.
Then we can iterate through each row to get each number.
The innermost body of the nested loops represents one individual element.
The numbers variable is holding a list[list[int]], which means that the row will become a list[int] (the element type of the outer list).
The number variable will then be an int, since that is the element type of the inner list.
nested = [[4, 6, 8],
[3, 5, 3]]
total = 0
for row in nested:
row_total = 0
for value in row:
row_total = row_total + value
total = total + row_total
print(total)
Doing something with the individual elements, however, requires state outside of the loops.
Much like how our loop patterns had an additional variable outside of the loop, we require additional variables outside of both loops depending on what we are trying to do.
In the following code, we are adding up all the numbers in the list, first by iterating through row and summing their values, while adding together all those nested sums.
Technically, in this case, we could have added directly to the total variable, but we will soon see another example where we MUST have inner accumulation variables.
def double_table(table: list[list[int]]) -> list[list[int]]:
new_list = []
for row in table:
new_row = []
for value in row:
new_row.append(value * 2)
new_list.append(new_row)
return new_list
print(double_table([[4, 6, 8],
[3, 5, 3]]))
In this function double_table, we are implementing the nested map pattern by iterating through the list of lists of integers given.
For each row, we must create a new_row.
We also need to append each new_row to a new_list in each of the outermost iterations.
When we are done, we’ll have perfectly recreated the original structure, but this time each of the original numbers will have been doubled.
nested = [[4, 6, 8],
[3, 5, 3]]
found = None
for row in nested:
for value in row:
if value > 5:
found = value
print(found)
In this last nested loop pattern, we see a modification of the find pattern where we find the last value greater than 5.
We are once again iterating through each and then through each element within that row.
With the if statement inside of the innermost for loop, we ask a question about each individual element.
Because we are seeking a property about the entire table and not just within each row, we can use the same found variable throughout the entire nested iteration.
grid = [
[1, 2, 3, 5, 6],
[7, 8, 9, 10, 11],
[12, 13, 14, 15, 16],
[17, 18, 19, 20, 21],
]
for y, row in enumerate(grid):
for x, value in enumerate(row):
print(x, y, value)
Combining what we have learned about iterating through indexes with what we know now about iterating through 2-dimensional lists, we can iterate through the indexes of the grid of data to get access to the X- and Y-positions within the table.
Note how we iterate vertically through the table in the outermost loop, meaning that each index represents the y coordinate in the table.
The inner loop iterates within a row, so that represents the column or x coordinate.
Inside the loops, we have access to each individual x- and y-coordinate.
grid = [ [1, 2, 3, 5, 6], [7, 8, 9, 10, 11], [12, 13, 14, 15, 16], [17, 18, 19, 20, 21], ] # Read third row, second column print(grid[2][1]) # Write third row, second column grid[2][1] = 100 print(grid[2][1])
Instead of iterating by position, we can also access and update specific values within the grid by chaining together the indexing notation.
This is like how we chain together attribute accesses or method calls.
Each time we reach another layer of data, we must unpack the information with another index.
The expression grid[2] gives a list, so we can unpack that with another indexing by having grid[2][1].
Much like how we iterated vertically and then horizontally, it’s important to remember that the indexing happens with the Y-axis first, then the X-axis second.
def sum_row(numbers: list[int]) -> int:
total = 0
for number in numbers:
total = total + number
return total
def map_sum(grid: list[list[int]]) -> list[int]:
result = []
for row in grid:
result.append(sum_row(row))
return result
print(map_sum([
[1, 2, 3, 5, 6],
[7, 8, 9, 10, 11],
[12, 13, 14, 15, 16],
[17, 18, 19, 20, 21],
]))
Decomposing complex grid-handling functions with a helper function is a smart way to make complicated code easier to read. Instead of trying to do two different loops, we can have one of the helper functions in a separate loop. This example shows how we can turn a list of lists of integers into a single list of integers by mapping a sum function to each row. This same idea can be applied to many other 2D list operations.
for loop inside of a for loop.for loop patterns can be adapted to nested lists by nesting layers of for loops.for loop operations.