Python Part I · Module 5 · Lists
Lists Inside Lists
Chapter 5.1 said a list can hold any kind of value, and a list is a kind of value — so a list can hold lists. That sounds like a curiosity and is actually the most useful structure in the module, because it is how you hold a table. A row of results is a list; a table of results is a list of those. Chapter 5.6 ran into this accidentally and badly, when repeating a list of lists produced one row referred to twice. This chapter does it deliberately: how to write a table down, how to reach a single cell with two indexes, and the important difference between changing one cell and replacing a whole row.
Foundation13 min readPythonPart IFundamentalsListsNesting
Part I · Module 5 · Chapter 5.7 · Lists Inside Lists
1. A Table Is a List of Rows
One test result has several parts — a name, a status, a runtime. That is a list:
row = ["smoke", "PASS", 12.4]Several results is a list of those:
table = [["smoke", "PASS"], ["reset", "FAIL"]]
print(table)
print(len(table))[['smoke', 'PASS'], ['reset', 'FAIL']]
2Read that length carefully, because it is the first thing to get right. len(table) is 2, not 4. The outer list has two elements, and each of those elements happens to be a list of two strings. Counting elements means counting at one level.
Nothing new is happening. Chapter 5.1 said an element can be any value; here the values are lists.
2. Seeing the Structure
Written on one line, a nested list is hard to read. Written out with one row per line it explains itself, and Python accepts the line breaks inside the brackets:
results = [
["smoke", "PASS", 12.4],
["reset", "FAIL", 8.7],
]
print(results)
print(len(results))
print(len(results[0]))[['smoke', 'PASS', 12.4], ['reset', 'FAIL', 8.7]]
2
3Two rows of three. The trailing comma after the last row is allowed and is worth adopting — adding a row later becomes a one-line change.
The structure, drawn out:
results (a list — 2 elements)
├─ [0] ──→ ["smoke", "PASS", 12.4] (a list — 3 elements)
│ ├─ [0] "smoke"
│ ├─ [1] "PASS"
│ └─ [2] 12.4
└─ [1] ──→ ["reset", "FAIL", 8.7] (a list — 3 elements)
├─ [0] "reset"
├─ [1] "FAIL"
└─ [2] 8.7There are two levels of numbering and each level starts at zero independently. The outer numbers pick a row; the inner numbers pick a position within that row.
3. Reaching a Cell
Two indexes get you one value:
table = [["smoke", "PASS"], ["reset", "FAIL"]]
print(table[1][0])
print(table[0][1])reset
PASSChapter 5.2 gave the rule this depends on and it has not changed: each bracket applies to whatever is on its left. Here it is with the middle step given a name so you can see it happen:
table = [["smoke", "PASS"], ["reset", "FAIL"]]
row = table[1]
print(row)
print(row[0])['reset', 'FAIL']
resettable[1] evaluates first and produces a list — the second row. Then [0] is applied to that list and produces its first element. table[1][0] is those two steps written without stopping in between; it is not a single two-dimensional lookup.
So the order is always: row first, then position within the row. table[1][0] is row 1, item 0.
And because the middle result is an ordinary list, everything from the module applies to it:
table = [["smoke", "PASS"], ["reset", "FAIL"]]
print(len(table[0]))
print(table[0][-1])
print(table[1][:1])2
PASS
['reset']len of a row, negative indexing within a row, a slice of a row — none of which needed new rules.
4. A Realistic Table
With three columns, reaching a specific cell means knowing what each position means:
results = [
["smoke", "PASS", 12.4],
["reset", "FAIL", 8.7],
]
print(results[1][2])
print(results[0][0])8.7
smokeresults[1][2] is row 1, position 2 — the runtime of the reset test. results[0][0] is the name of the first test.
5. Changing a Cell, and Changing a Row
Everything Chapter 5.3 established about mutation applies at both levels, and the two operations look almost identical.
Change one cell — index to the row, then assign to a position in it:
results = [
["smoke", "PASS", 12.4],
["reset", "FAIL", 8.7],
]
print(results[1])
results[1][1] = "PASS"
print(results[1])
print(results)['reset', 'FAIL', 8.7]
['reset', 'PASS', 8.7]
[['smoke', 'PASS', 12.4], ['reset', 'PASS', 8.7]]One value changed, in place, inside the second row. The table still has two rows of three.
Replace a whole row — assign to a position in the outer list:
results = [
["smoke", "PASS", 12.4],
["reset", "FAIL", 8.7],
]
results[0] = ["boot", "PASS", 1.0]
print(results)[['boot', 'PASS', 1.0], ['reset', 'FAIL', 8.7]]The difference is one bracket. results[0] = [...] puts a new list at position 0 of the table; results[0][1] = "..." reaches into the list already there and changes one of its elements.
And adding a row is just append at the outer level:
results = [
["smoke", "PASS", 12.4],
["reset", "FAIL", 8.7],
]
results.append(["burst", "FAIL", 20.1])
print(len(results))
print(results[-1])3
['burst', 'FAIL', 20.1]Three rows now, and results[-1] is the last one — negative indexing at the outer level, picking a row.
6. Rows Need Not Match
Python does not require the inner lists to be the same length, because it does not know they are meant to be rows:
ragged = [["a"], ["b", "c", "d"]]
print(len(ragged), len(ragged[0]), len(ragged[1]))2 1 3Two elements at the outer level, of length 1 and 3. This is allowed and is occasionally what you want. When it is not what you want — a table where one row is short — nothing will warn you, and the failure appears later as an IndexError on the row that happens to be short:
t = [["a"]]
print(t[1][0])IndexError: list index out of rangeNote which bracket failed. t[1] is out of range at the outer level — there is only one row — and the second bracket never ran. When a nested access raises, work out which level is at fault before changing anything; printing the intermediate value, as Section 3 did, is the fastest way.
7. Membership Looks at Whole Elements
Chapter 5.1 said in on a list compares whole elements. Nesting is where that bites:
table = [["smoke", "PASS"], ["reset", "FAIL"]]
print(["smoke", "PASS"] in table)
print("smoke" in table)True
FalseThe first is True — ["smoke", "PASS"] really is one of the two elements.
The second is False, and it looks wrong. "smoke" is plainly in there. But it is not an element of table — the elements of table are two lists, and neither of them is equal to the string "smoke". Membership does not search inside elements.
8. What Is Missing
You can now build a table, reach any cell, change a cell, replace a row and add a row. What you cannot yet do is work through the whole table without naming each row by number — checking every result, counting the failures, printing each line of a report.
That is iteration, and for a nested structure it is nested iteration: going through the rows, and for each row going through its items. Module 10 — Loops covers it, and Chapter 10.11 deals specifically with nested structures. The structure you have built here is what those loops will be given; nothing you have learned in this chapter changes when they arrive.
9. Predict, Run, Observe
Predict every line before reading on. Two of these are the traps.
grid = [["a", "b"], ["c", "d"], ["e", "f"]]
print(len(grid))
print(len(grid[0]))
print(grid[2][1])
print(grid[-1][0])
print(grid[0][-1])
print("c" in grid)
print(["c", "d"] in grid)
print(grid[1])The verified output:
3
2
f
e
b
False
True
['c', 'd']len(grid) is 3 — three rows, not six values. grid[2][1] is row 2, position 1: "f". grid[-1][0] is the last row's first item, "e" — negative indexing works at either level. grid[0][-1] is the first row's last item, "b".
The two membership lines are Section 7: "c" is not an element of grid, so False; the row ["c", "d"] is, so True. If you predicted True for "c" in grid you expected in to look inside the rows, which it never does.
10. Exercises
Exercise 1 — build a table. Write a list of three rows, each holding a signal name and a width as a number. Print the whole table, its length, and the length of the first row.
Exercise 2 — reach the cells. For t = [["clk", 1], ["addr", 32], ["data", 64]], write expressions producing: the name in the second row, the width in the last row, the first row as a whole, and the number of rows.
Exercise 3 — predict. Given m = [[1, 2, 3], [4, 5, 6]], give each result without running it, then verify:
(a) len(m) (b) len(m[1]) (c) m[0][2] (d) m[1][-1] (e) m[-1][0] (f) m[0]
Exercise 4 — cell or row. Starting from t = [["a", "b"], ["c", "d"]] each time, say what the table contains after each, and which level was changed:
(a) t[0][1] = "x" (b) t[0] = ["x"] (c) t.append(["e", "f"]) (d) t[1][0] = "x"
Exercise 5 — which bracket failed. This raises. Say which level is out of range, how you could tell, and what the corrected line would be if you wanted the last row's first item:
t = [["a", "b"], ["c", "d"]]
print(t[2][0])Exercise 6 — membership. For table = [["smoke", "PASS"], ["reset", "FAIL"]], say True or False for each and explain the two that surprise you:
(a) "reset" in table (b) ["reset", "FAIL"] in table (c) "reset" in table[1] (d) "FAIL" in table[0]
Exercise 7 — the column problem. A colleague stores results as ["smoke", "PASS", 12.4] and reads the runtime with row[2]. A new column is inserted before the runtime. Say what breaks, whether Python reports it, and one sentence on what you would do about it.
11. Summary
- A list can hold lists, which is how a table of rows is stored.
len()counts at one level. A two-row table of three columns haslen()2, not 6.- Write one row per line with a trailing comma — it reads as a table and adding a row stays a one-line change.
- Each bracket applies to whatever is on its left.
table[1][0]evaluatestable[1]to a row, then takes that row's first element. It is two operations, not one lookup. - Row first, then position within the row.
- A row is an ordinary list —
len, negative indexing, slicing and every method all work on it. table[0][1] = xchanges a cell;table[0] = [...]replaces a row. One bracket apart, different operations.table.append(row)adds a row, andtable[-1]is the last one.- Rows need not be the same length. Nothing warns you; the failure arrives later as an
IndexError. - When a nested access raises, find out which level failed by printing the intermediate value.
incompares whole elements and never looks inside them."smoke" in tableisFalse;"smoke" in table[0]isTrue.- Position numbers carry no meaning. Keep the column order documented, and note that Modules 6 and 8 offer better structures for records.
Two chapters have now shown one list being reachable two ways. The next chapter explains it.
Where this fits
Part of the Python curriculum.
