Python 2D Lists: Create, Traverse and Transpose a Grid

A Python grid is a list of row lists, indexed by row then column. Build a 2D list correctly, visit neighbours with a bounds check, and transpose with zip.

  • Course: Python study plan
  • Module: Lists, tuples and sequences
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: CPython 3.11

How do I create a 2D list in Python?

Create a 2D list in Python with a comprehension that builds a fresh row each time: grid = [[0] * C for _ in range(R)] makes R rows of C zeros, indexed grid[r][c] with the row first. Never write [[0] * C] * R — it repeats one row object R times, so setting grid[0][0] changes column 0 of every row.

Lesson

A two-dimensional grid — a game board, a matrix, a maze, a spreadsheet — is a list of rows, each row a list of cells, indexed grid[r][c] with the row first. Almost every interview problem over grids uses the same six operations: read it from input, build an empty one of a given size, walk every cell, visit a cell's neighbours without falling off the edge, transpose or rotate it, and print it back. This lesson gives each as a template, states the row-major convention and the [[0] * C] * R trap once more where it does the damage, and shows the flattening trick for problems that are one-dimensional in disguise.

Reading a grid

R, C = map(int, input().split())
grid = [list(map(int, input().split())) for _ in range(R)]      # numbers with spaces
maze = [list(input().rstrip("\n")) for _ in range(R)]           # characters, no spaces
maze = [input().rstrip("\n") for _ in range(R)]                 # rows as strings, if not modified

Rows as strings are fine for reading (maze[r][c] works) and immutable; convert each row to a list when cells must be changed. Trust the declared R; validate len(row) == C if the input might be ragged.

Building an empty grid

grid = [[0] * C for _ in range(R)]        # R independent rows
grid = [[0] * C] * R                      # WRONG: R references to one row

The second form is Module 6 lesson 1's trap in its natural habitat: writing grid[0][0] = 1 sets column 0 of every row. The comprehension runs [0] * C once per row and is the only correct spelling. For a grid of strings or None, the same shape: [[None] * C for _ in range(R)].

Walking every cell

for r in range(R):
    for c in range(C):
        cell = grid[r][c]

for r, row in enumerate(grid):            # when the row itself is useful
    for c, cell in enumerate(row):
        ...

for row in grid:                          # order does not matter, index not needed
    for cell in row:
        total += cell

sum(sum(row) for row in grid) sums a numeric grid; sum(row.count("#") for row in grid) counts a character; max(max(row) for row in grid) finds the largest. Column sums come from the transpose (below) or sum(grid[r][c] for r in range(R)).

Neighbours and bounds

DIRS4 = [(-1, 0), (1, 0), (0, -1), (0, 1)]                      # up, down, left, right
DIRS8 = [(dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1) if (dr, dc) != (0, 0)]

def neighbours(r, c):
    for dr, dc in DIRS4:
        nr, nc = r + dr, c + dc
        if 0 <= nr < R and 0 <= nc < C:
            yield nr, nc

The bounds check 0 <= nr < R and 0 <= nc < C is the line that prevents both IndexError and the subtler bug of a negative index silently wrapping to the last row (grid[-1] is valid Python). A direction list makes the four-way and eight-way cases one loop rather than four or eight ifs, and it is the shape flood fill, BFS on a grid and counting islands all use. Returning neighbours as a generator (yield, Module 11) keeps the caller's loop simple: for nr, nc in neighbours(r, c):.

Transposing and rotating

transposed = [list(col) for col in zip(*grid)]          # columns become rows
rotated_cw = [list(col) for col in zip(*grid[::-1])]    # rotate 90° clockwise
rotated_ccw = [list(col) for col in zip(*grid)][::-1]
flipped_h = [row[::-1] for row in grid]                 # mirror left–right
flipped_v = grid[::-1]                                  # mirror top–bottom (rows shared)

zip(*grid) is the transpose: the star spreads the rows as separate arguments, and zip pairs up their first elements, then their second, and so on. Column sums are [sum(col) for col in zip(*grid)]. For a ragged grid zip truncates to the shortest row.

Printing

for row in grid:
    print(" ".join(map(str, row)))        # numbers separated by spaces

for row in maze:
    print("".join(row))                   # characters, no separator

print("\n".join(" ".join(map(str, row)) for row in grid))    # one print

print(row) shows the list's repr with brackets and is almost never the expected format. Aligned columns use a format spec per cell: " ".join(f"{v:3}" for v in row).

Flattening and the index trick

A grid is also one list of R * C cells: cell (r, c) is index r * C + c, and index i is divmod(i, C). Flattening ([cell for row in grid for cell in row]) lets you sort every cell, find the k-th smallest, or use bisect; the index arithmetic lets you store a grid in a flat list or a bytearray when memory matters. Row-major order — all of row 0, then all of row 1 — is also the order the grid is read from input and printed to output, and the order in which walking with r outer and c inner visits cells.

Copying a grid

grid.copy() copies the outer list only; the rows are shared. [row[:] for row in grid] copies every row (a full copy for a grid of immutables); copy.deepcopy(grid) for anything nested deeper. A function that mutates a grid it was given should say so; a simulation that needs "the grid at the previous step" needs a real copy, not an alias.

Pitfalls

  • [[0] * C] * R.
  • grid[c][r] — row first, always.
  • A missing bounds check, and negative indexes wrapping instead of failing.
  • print(row).
  • Modifying the grid while walking it and reading cells already changed this step — use a copy or a second grid.
  • Comparing a row string with a row list ("..." != [".", ".", "."]).

Key takeaways

  • A grid is a list of row lists, grid[r][c], row-major; read rows with a comprehension over range(R).
  • Build with [[0] * C for _ in range(R)]; copy rows with [row[:] for row in grid].
  • Walk with nested range or enumerate; neighbours through a direction list and the bounds check 0 <= nr < R and 0 <= nc < C.
  • zip(*grid) transposes; zip(*grid[::-1]) rotates; row[::-1] mirrors.
  • Print with " ".join(map(str, row)); flatten with a double comprehension; (r, c) ↔ r * C + c.

Common questions

How do I transpose a matrix in Python?

Use zip(*grid): the star passes each row as a separate argument, and zip groups their first elements, then their second, and so on. [list(col) for col in zip(*grid)] returns the columns as lists; for a ragged grid, zip truncates to the shortest row.

How do I rotate a matrix 90 degrees in Python?

Reverse the rows, then transpose: [list(col) for col in zip(*grid[::-1])] rotates clockwise. Transposing first and reversing the result, [list(col) for col in zip(*grid)][::-1], rotates anticlockwise, and [row[::-1] for row in grid] mirrors left to right.

How do I get the neighbours of a cell in a grid in Python?

Loop over a list of direction offsets such as [(-1, 0), (1, 0), (0, -1), (0, 1)] and keep a neighbour only when 0 <= nr < R and 0 <= nc < C. The bounds check prevents IndexError and stops a negative index silently wrapping round to the last row.

How do I copy a 2D list in Python?

[row[:] for row in grid] copies every row, which is a full copy for a grid of numbers or strings. grid.copy() copies only the outer list, so the rows are still shared; use copy.deepcopy(grid) when the structure is nested more deeply.

How do I print a 2D list without brackets?

Join each row: print(" ".join(map(str, row))) prints numbers separated by spaces, and "".join(row) prints a row of characters with no separator. print(row) shows the list's repr, brackets and quotes included, which is rarely the expected output.

Exercises

One step of Life

Read an R × C grid of # (live) and . (dead) cells and print the next generation of Conway's Game of Life: a live cell with 2 or 3 live neighbours (of its 8) stays alive, a dead cell with exactly 3 becomes alive, every other cell is dead. Use a direction list, a bounds check, and a new grid — never write into the grid you are reading.

Input: R C, then R rows. Output: R rows.

3 3
.#.
.#.
.#.

prints

...
###
...

Transpose and rotate

Read an R × C grid of integers and print its transpose, then the grid rotated 90° clockwise, then the column sums — each built with zip(*...). Separate the three blocks with a line --.

Input: R C, then R rows. Output: the transpose (C rows), --, the rotation (C rows), --, one line of C column sums.

2 3
1 2 3
4 5 6

prints

1 4
2 5
3 6
--
4 1
5 2
6 3
--
5 7 9

In this module: Lists, tuples and sequences

← Tuples, unpacking and named tuples · The sequence tools — enumerate, zip, reversed, any, all, deque and the protocol →