Lists of Lists

🧩 Lists of Lists (2D Lists as a Special Case)

This week's class covered "2D lists" — but the actual examples were a mix of two different things: lists that genuinely behave like a fixed grid, and lists that are just generally nested but irregular. The more accurate way to think about it: a list of lists is the general Python concept (any list containing other lists, of any lengths). A 2D list is the well-behaved special case where every inner list is the same length — that's what actually earns the name "2D," since only then does row, col indexing describe a real grid.

⚡ Coming from Java Java's int[][] grid = new int[4][4] is a TRUE 2D array — rectangular by construction. You can't accidentally make one row longer than another; the shape is fixed at creation. Python has no built-in 2D array type at all — what looks like a "2D list" is really just an outer list whose elements happen to all be lists of the same length. Nothing stops you from breaking that — Python won't complain if row 3 has a different length than row 1, which is exactly the trap in this week's class material (see the Gotchas section).
🚀 FastAPI relevance — MODERATE Lists of lists map directly onto nested JSON arrays. A grid of game-board state, a matrix of scores, a table of rows — any of these returned from a FastAPI route just becomes [[...], [...], ...] in the JSON response with zero extra work. The same jagged-vs-rectangular distinction from this lesson matters there too: a frontend expecting a fixed grid will break if your API ever returns rows of inconsistent length.

🏗️ Building and Reading the Structure

Both examples below come from this week's actual class material.

Rectangular — a true 2D list
grid[row][col]
Every inner list has the same length (4 here). This is the case that genuinely deserves to be called "2D" — it behaves like a real grid, exactly like the class's grid example.
li = [ [23, 45, 67, 85], [34, 98, 87, 22], [11, 13, 12, 46], [73, 67, 97, 81], ] li[1][2] # 87 — row 1, column 2
Jagged — a general list of lists
NOT a 2D list
Rows here have different lengths (2, 3, then 1). This is the class's "nums" example — it's a perfectly valid list of lists, but calling it 2D is misleading, since there's no consistent row/column shape.
nums = [[10, 20], [30, 40, 50], [60]] len(nums[0]) # 2 len(nums[1]) # 3 — different! not rectangular
Outer vs inner length
len(li) vs len(li[i])
len(li) tells you the number of rows. len(li[i]) tells you the length of row i specifically. For a true 2D list these are the same for every row — for a jagged list they're not, which is the whole distinction.
len(li) # 4 — number of rows len(li[0]) # 4 — length of row 0 len(li[2]) # also 4 — same for every row, since li is rectangular
Creating a true 2D list safely
comprehension, not *
A common trap when building a fixed-size 2D list from scratch (e.g. for a chess board or Minesweeper grid) — see the Gotchas box for why [[0] * cols] * rows is broken, and use a list comprehension instead.
# Build a 3x3 grid of zeros, safely rows, cols = 3, 3 board = [[0 for _ in range(cols)] for _ in range(rows)] # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] — three INDEPENDENT rows

🔄 Iterating — the Class's Approach, and a More Pythonic One

The class material used nested while loops throughout — that works, and is genuinely closer to how you'd do this in Java. Here's the same logic next to Python's more idiomatic for-based approach.

Printing Every Element — while loop (the class's version) vs for loop
Both produce identical output. The for-loop version needs no manual index variables or manual incrementing at all.
# The class's version — nested while loops i = 0 while i < len(li): j = 0 while j < len(li[i]): print(li[i][j]) j += 1 i += 1 # The same thing, the Pythonic way — no index bookkeeping needed for row in li: for num in row: print(num)
Counting Total Elements — the jagged "nums" example
This is the class's exact "count the total numbers" practice question, using the jagged nums list. Both versions below give the same answer (6).
nums = [[10, 20], [30, 40, 50], [60]] # The class's version — accumulate with a while loop i = 0 count = 0 while i < len(nums): count += len(nums[i]) i += 1 print("The number of numbers is ", count) # The same thing, in one line count = sum(len(row) for row in nums) # → 6
Pairing Rows Together — the "animals" example, with zip()
The class's animals example pairs up entries from two PARALLEL rows by matching index — exactly the situation Python's built-in zip() exists for. It reads more clearly and removes the index variable entirely.
animals = [ ["Dog", "Cat", "Cow", "Lion"], ["Puppy", "Kitten", "Calf", "Cub"], ] # The class's version — index-based while loop index = 0 while index < len(animals[0]): print("The mummy is a ", animals[0][index], " and the baby is a ", animals[1][index]) index += 1 # With zip() — pairs up the two rows directly, no index needed for mummy, baby in zip(animals[0], animals[1]): print(f"The mummy is a {mummy} and the baby is a {baby}")

📋 Quick Reference

OperationSyntaxNotes
Create (literal)[[1,2],[3,4]]A list of lists — rectangular ONLY if you keep row lengths consistent
Create a true 2D grid safely[[0]*cols for _ in range(rows)]List comprehension — each row is independent (see Gotchas)
Access an elementgrid[row][col]Outer index first, then inner
Number of rowslen(grid)Outer list length
Length of a specific rowlen(grid[i])Only the same as len(grid[0]) if rectangular
Iterate every elementfor row in grid:
    for x in row:
No index variables needed
Total element countsum(len(row) for row in grid)Works whether rectangular or jagged
Pair up two parallel rowszip(row_a, row_b)Cleaner than index-based pairing
Check if truly rectangularlen(set(len(r) for r in grid)) == 1True only if every row is the same length
⚠ Gotchas for Java / JavaScript Programmers:

"2D list" is not enforced — it's just a convention you have to maintain yourself — unlike Java's int[][], Python will happily let row lengths drift apart, which is exactly what happened in this week's class material with the nums example. If your code assumes every row is the same length (e.g. using len(grid[0]) as "the" row length for all rows), a jagged list will silently produce wrong results rather than an error.

[[0] * cols] * rows is a classic trap — this creates ONE inner list, then repeats the same REFERENCE to it rows times. Changing board[0][0] changes every row's first element, because they're all secretly the same list. Always build rows independently with a list comprehension: [[0]*cols for _ in range(rows)].

Mixing up rows and columnsgrid[i][j] means row i, column j — easy to flip by accident, especially when porting logic from a language where you write grid[i, j] instead. There's no compiler to catch the mistake; it'll just read/write the wrong cell silently.