PYTHON / LISTS AND TUPLES
Nested lists and two-dimensional data
Model a table as a list of row lists, index and mutate it with grid[r][c], build grids safely, and gather columns.
What you will learn
- Read grid[r][c] as two lookups: pick a row object, then index inside it
- Build fresh rows with [[0] * cols for _ in range(rows)], never [[0] * cols] * rows
- Get a column with [row[c] for row in grid]; rows are cheap, columns are gathered
- Use enumerate on both loops when you need the r and c of each cell
Understanding Nested lists and two-dimensional data
Python has no built-in table type, so two-dimensional data is stored as a list whose elements are themselves lists. The outer list holds references to row objects, not the numbers themselves, which is why grid[1][2] is not special syntax: grid[1] evaluates to a row list, and [2] then indexes that row. Nothing checks that the rows are the same length or even that they are lists at all, so a nested list is a convention you maintain, not a shape Python enforces.
That reference-based layout makes rows and columns behave very differently. A row already exists as a single object, so grid[0] hands you something you can mutate in place and see reflected in the grid. A column has no object of its own; it exists only as one value from each row, so you must collect it with something like [row[2] for row in grid], and the resulting list is a fresh copy whose changes do not reach back into the grid.
The same reasoning explains the classic construction bug. [[0] * 3] * 3 repeats one row reference three times, so all three slots point to the same list and writing to one cell appears in every row. A comprehension such as [[0] * 3 for _ in range(3)] evaluates the inner list expression once per iteration and produces three separate row objects, which is what you almost always want.
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
print(grid[1])
print(grid[1][2])
grid[0][0] = 99
print(grid[0])
for row in grid:
print(" ".join(f"{value:3d}" for value in row))
print(len(grid), len(grid[0]))
column = [row[1] for row in grid]
print(column)
A nested list is a list of references to other lists, so grid[r][c] is two chained lookups and identical-looking rows may be the same object.
Worked examples
Repetition shares rows, comprehension does not
Shows why multiplying a list of lists produces rows that all change together.
bad = [[0] * 3] * 3
bad[0][0] = 1
print(bad)
good = [[0] * 3 for _ in range(3)]
good[0][0] = 1
print(good)
print(bad[0] is bad[1])
print(good[0] is good[1])
Example explained
Line 1[[0] * 3] * 3 copies the single inner list reference three times, so there is only one row in memory.
Line 2bad[0][0] = 1 mutates that one shared row, which is visible through all three outer slots.
Line 3The comprehension re-evaluates [0] * 3 on each pass, creating three distinct row objects.
Line 4The `is` checks confirm the difference: sharing versus separate identity, not equal versus unequal values.
Ragged rows and cell coordinates
Iterates a grid whose rows have different lengths, tracking row and column indices.
rows = [
[1, 2, 3],
[4],
[5, 6],
]
print(sum(len(row) for row in rows))
for r, row in enumerate(rows):
for c, value in enumerate(row):
print(r, c, value)
if len(rows[1]) > 1:
print(rows[1][1])
else:
print("missing")
Example explained
Line 1sum(len(row) for row in rows) counts cells, since len(rows) alone only counts rows.
Line 2The inner loop runs over len(row) items, so a ragged grid iterates correctly without extra checks.
Line 3rows[1][1] would raise IndexError, so the length guard is needed before touching a cell in a short row.
Transposing with zip
Turns rows into columns and checks whether the result stays linked to the original.
matrix = [
[1, 2, 3],
[4, 5, 6],
]
transposed = [list(col) for col in zip(*matrix)]
print(transposed)
matrix[0][0] = 100
print(transposed[0])
print(matrix)
Example explained
Line 1zip(*matrix) passes each row as a separate argument, so zip pairs up the first items, then the second, and so on.
Line 2zip yields tuples, so list(col) is needed if the new grid must be mutable.
Line 3transposed holds copies of the integer values, so changing matrix[0][0] afterwards leaves transposed unchanged.
Line 4zip stops at the shortest row, so a ragged grid silently loses trailing cells here.
Replacing a row in place versus rebinding
Shows why assigning to the loop variable does not modify the grid.
grid = [[1, 1], [2, 2], [3, 3]]
for row in grid:
row = [0, 0]
print(grid)
for row in grid:
row[:] = [0, 0]
print(grid)
Example explained
Line 1row = [0, 0] only points the loop name at a new list; the grid still references the old rows.
Line 2row[:] = [0, 0] overwrites the contents of the row object the grid points to, so the change sticks.
Line 3grid[i] = [0, 0] with an index would also work, because it writes into the outer list.
Important notes
A list of lists is not a matrix: grid[1, 2] raises TypeError, and grid1 + grid2 concatenates rows instead of adding numbers. Use numpy when you need real array math.
Nothing guarantees equal row lengths, so validate with len(row) or use len(grid[0]) only after you know the grid is rectangular.
Common mistakes
Building a grid with [[0] * 3] * 3: every row is the same object, so setting one cell changes the whole column-wide picture and the bug shows up far from the assignment.
Swapping the indices and writing grid[col][row]: on a non-square grid this raises IndexError, and on a square grid it silently reads the transposed cell.
Assigning to the loop variable (for row in grid: row = [0, 0]) and expecting the grid to change; the grid is untouched because only the local name was rebound.
Try it yourself
Change, predict, then run
Build a 3x4 grid of zeros with a comprehension, set grid[i][i] to 1 for i in range(3), print each row on its own line, then print the four column sums as a list.
Open the Python workspaceCheck your understanding
With grid = [[0] * 2] * 2, why does grid[0][0] = 5 produce [[5, 0], [5, 0]] while grid[0] = [9, 9] produces [[9, 9], [0, 0]]?
- grid[0][0] = 5 mutates the one inner list both slots reference, while grid[0] = [9, 9] only repoints slot 0 to a new list and leaves slot 1 on the old one
- The * operator copies the inner list, so the rows are separate; the different results come from the order the two assignments run in
- Assignment to a nested index is broadcast to all rows by design, whereas assignment to a whole row is not
- grid[0] = [9, 9] edits the row in place, while grid[0][0] = 5 forces Python to copy the row first
Show answer
The repetition made one row object referenced twice, so mutating through grid[0][0] is visible via grid[1]; grid[0] = [9, 9] is a write to the outer list that swaps in a brand-new object for one slot only. Option 2 is tempting because the printed rows look independent, but * copies references, not the list contents, which is exactly what the first result proves.