Python Part I · Module 10 · Loops
Nested Loops: Every Test at Every Seed
A regression is rarely a list of tests. It is a list of tests run at a list of seeds, which is a different and much larger thing: forty tests at a hundred seeds is four thousand simulations, and the code that expresses it is one line longer than the code for forty. That asymmetry is what makes nested loops worth a chapter of their own. The syntax is nothing new — a loop is a statement, and a loop body holds statements — but two things about the result surprise people reliably: the order the passes actually happen in, and how many of them there are.
Foundation16 min readPythonPart IFundamentalsLoopsNesting
Part I · Module 10 · Chapter 10.11 · Nested Loops: Every Test at Every Seed
1. Two Lists, One Job List
You have tests and you have seeds, and you want every combination:
tests = ["smoke", "reset"]
seeds = [4210, 4211, 4212]Six runs. Writing them out is already tedious at this size and impossible at the real one.
2. A Loop Inside a Loop
tests = ["smoke", "reset"]
seeds = [4210, 4211, 4212]
runs = 0
for test in tests:
for seed in seeds:
runs += 1
print(runs, test, seed)
print("inner body ran", runs, "times =", len(tests), "x", len(seeds))1 smoke 4210
2 smoke 4211
3 smoke 4212
4 reset 4210
5 reset 4211
6 reset 4212
inner body ran 6 times = 2 x 3Nothing new was introduced. The inner for is a statement, and the outer loop's body holds statements — Chapter 9.2's block rule, applied to a loop instead of an if. The indentation does all the work: the inner loop is at column 4 so it belongs to the outer body, and runs += 1 is at column 8 so it belongs to the inner body.
3. The Order: The Inner Loop Varies Fastest
Read the output again. smoke appears three times in a row, then reset three times. The seed changes on every line; the test changes only when the seeds run out.
That is the rule: the inner loop completes entirely for each single pass of the outer one.
The restart is worth seeing explicitly, because "the inner loop runs again" is easy to say and easy to picture wrongly:
for test in ["smoke", "reset"]:
print("outer pass for", test)
for seed in [1, 2]:
print(" inner", seed)
print(" inner loop finished")outer pass for smoke
inner 1
inner 2
inner loop finished
outer pass for reset
inner 1
inner 2
inner loop finishedThe inner loop ran to completion twice, from the beginning both times. It does not resume where it left off, and the inner collection is not consumed — Chapter 10.1 Section 7 established that looping does not use a collection up, and this is where that matters.
4. The Count Is a Multiplication
The inner body runs once per combination: the length of the outer collection times the length of the inner one.
tests = ["smoke", "reset"]
seeds = [4210, 4211, 4212]
runs = 0
for test in tests:
for seed in seeds:
runs += 1
print(runs, "=", len(tests), "x", len(seeds))6 = 2 x 3Three levels multiply three times:
runs = 0
for tool in ["vcs", "xrun"]:
for test in ["smoke", "reset"]:
for seed in [1, 2, 3]:
runs += 1
print("combinations:", runs)combinations: 12Two tools, two tests, three seeds — twelve runs, from a program whose three collections total seven items.
5. Building the Job List
The accumulator pattern from Chapter 10.10 works unchanged; only its position needs care:
tests = ["smoke", "reset"]
seeds = [4210, 4211]
jobs = []
for test in tests:
for seed in seeds:
jobs.append((test, seed))
print(jobs)
print(len(jobs), "jobs")[('smoke', 4210), ('smoke', 4211), ('reset', 4210), ('reset', 4211)]
4 jobsjobs = [] is outside both loops, because the result belongs to the whole expansion. Putting it between the two for lines would reset it on every outer pass, which is Chapter 10.10 Section 3's bug with an extra level to hide in — and there it would leave you with only the last test's jobs.
6. break Leaves One Loop
Chapter 10.7 established this; here is where it bites:
for test in ["smoke", "reset"]:
for seed in [1, 2, 3]:
if seed == 2:
break
print(test, seed)
print("done")smoke 1
reset 1
doneThe inner loop broke on both outer passes, and the outer loop carried on regardless. There is no statement that leaves two loops at once.
When you genuinely need to stop everything — the first fatal in any test at any seed means the rest of the sweep is pointless — the available answer is a flag checked by the outer loop:
stop = False
for test in ["smoke", "reset"]:
for seed in [1, 2, 3]:
if test == "reset" and seed == 1:
stop = True
break
print(test, seed)
if stop:
break
print("done")smoke 1
smoke 2
smoke 3
doneTwo breaks and a flag: the inner one leaves the inner loop, the flag carries the decision outward, and the outer one acts on it.
7. What Nesting Costs
The asymmetry is the thing to carry away. Adding one level costs one line to write and a multiplication to run:
| Outer | Inner | Inner body runs |
|---|---|---|
| 2 tests | 3 seeds | 6 |
| 10 tests | 10 seeds | 100 |
| 40 tests | 100 seeds | 4,000 |
| 40 tests | 100 seeds × 2 tools | 8,000 |
At four thousand simulations of ninety seconds each, that nest is four days of machine time on one host. Nothing in the code looks four days long.
This is not an argument against nesting — expanding a sweep is exactly what the construct is for, and a regression grid is a real requirement. It is an argument for knowing the number before you run it. Printing len(tests) * len(seeds) before submitting anything costs one line and has saved a great many wasted nights.
tests = ["smoke", "reset", "burst", "cdc"]
seeds = [4210, 4211, 4212]
print("about to submit", len(tests) * len(seeds), "runs")about to submit 12 runs8. When a Nest Is the Wrong Shape
A nest produces every combination. If the pairs you want are not every combination — because some are excluded, or because they already exist as data — then one loop is correct and nesting is actively wrong:
jobs = [("smoke", 4210), ("smoke", 4211), ("reset", 4210)]
for test, seed in jobs:
print(test, seed)
print("one loop, three passes")smoke 4210
smoke 4211
reset 4210Three jobs, not four. reset at 4211 is deliberately absent — perhaps it is a known failure — and no nest can express that, because a nest has no way to omit a combination without an if inside it.
The rule: nest to generate combinations; loop once over pairs that already exist. Chapter 6.5's unpacking in the for line is what makes the second read as well as the first.
Chapter 10.3 Section 9 is the related case worth re-reading: a list of dictionaries is walked with one loop, because reaching inside each item is a lookup rather than an iteration. Needing a second loop means the inner thing is itself a collection you must visit every element of.
9. Predict, Run, Observe
# A
count = 0
for a in [1, 2]:
for b in [10, 20, 30]:
count += 1
print("A:", count)
# B
for a in ["x", "y"]:
for b in [1, 2]:
print("B:", a, b)
# C
total = 0
for a in [1, 2, 3]:
for b in [10, 20]:
total += a * b
print("C:", total)
# D
pairs = []
for a in [1, 2]:
for b in ["p", "q"]:
pairs.append((a, b))
print("D:", pairs)The verified output:
A: 6
B: x 1
B: x 2
B: y 1
B: y 2
C: 180
D: [(1, 'p'), (1, 'q'), (2, 'p'), (2, 'q')]A — 2 × 3.
B — the inner name varies fastest; x is paired with both values before y appears at all.
C — six combinations, so 10 + 20 + 20 + 40 + 30 + 60, which is 180. It also equals (1 + 2 + 3) × (10 + 20), because every a meets every b exactly once — a useful check, and one that only holds because a nest really does produce the full grid.
D — four pairs in the order Section 3 predicts.
10. Exercises
Exercise 1 — count before running. For 7 tests, 25 seeds and 2 simulators, give the number of runs without writing a loop. Then write the three-level nest with a counter and confirm it.
Exercise 2 — predict the order. Write down the first four lines of output for a nest over ["a", "b"] and [1, 2, 3], then verify. Say in one sentence which name changes on every line.
Exercise 3 — the restart. Add a line that prints inner finished after the inner loop but inside the outer one. Predict how many times it appears for a 3 × 4 nest, then verify.
Exercise 4 — build the grid. Build the full job list for ["smoke", "reset", "burst"] across seeds range(4210, 4213) as a list of tuples. Print the first job, the last job and the total.
Exercise 5 — the misplaced accumulator. Take Exercise 4 and move jobs = [] to between the two for lines. Predict the output, verify, and explain in two sentences which jobs survived and why.
Exercise 6 — leave both loops. Write a nest over tests and seeds that stops entirely at the first combination where the seed is 4212, printing everything before it. Verify, then say what you would write instead once you have functions.
Exercise 7 — nest or not. For each, say whether it wants a nest or one loop: (a) every test at every seed (b) a list of already-chosen test-and-seed pairs (c) every character of every signal name in a list (d) a list of dictionaries, printing one field of each.
Exercise 8 — exclude a combination. Expand two tests across three seeds, but skip reset at seed 4211. Write it as a nest with a continue, then as one loop over an explicit list of pairs, and say which you would prefer if the exclusion list grew to twenty entries.
11. Summary
- A loop inside a loop is not new syntax — a loop is a statement, and a body holds statements. The indentation says which body each line belongs to.
- The inner loop varies fastest, completing entirely for each single pass of the outer one.
- The inner loop restarts from the beginning on every outer pass; the inner collection is not consumed.
- The inner body runs
len(outer) × len(inner)times — a multiplication, and it compounds with each extra level. - An accumulator for the whole expansion goes outside both loops. Between them, it resets on every outer pass.
breakleaves one loop. Leaving both needs a flag and a secondbreak; Module 11's functions give a cleaner answer.- One extra line of code multiplies the runtime. 40 tests at 100 seeds is 4,000 runs, and nothing about the code looks that large.
- Print the product before submitting anything.
- Nest to generate every combination; loop once over pairs that already exist. A list of tuples wants one loop and unpacking in the
forline.
The last chapter of the module collects the mistakes that loops make available, including the two that produce no error at all.
Where this fits
Part of the Python curriculum.
