Python Part I · Module 10 · Loops
What Iteration Means
Every program you have written runs each line at most once. Module 9 let a program choose which lines, and that is still not enough: a regression has forty tests, a log has two million lines, and a seed sweep has a hundred values. Writing them out is not an option, and copying three lines forty times is the kind of code that develops bugs in thirty-nine places. This chapter adds repetition driven by data — a loop that runs its body once for each item in a collection. The syntax takes one line. The thing worth getting exactly right is what the loop variable is, because almost everyone's first model of it is wrong in a way that produces working code until the day it does not.
Foundation15 min readPythonPart IFundamentalsLoopsIteration
Part I · Module 10 · Chapter 10.1 · What Iteration Means
1. The Same Three Lines, Three Times
Here is a regression suite and the thing you want to do to each test in it:
tests = ["smoke", "reset", "burst"]
print(tests[0])
print(tests[1])
print(tests[2])smoke
reset
burstIt works, and everything about it is wrong as a method. The number 3 is written into the code in three places, so adding a fourth test means editing the program. Remove a test and tests[2] raises. The code describes the positions you happened to have rather than the thing you meant, which was: do this to every test.
2. One Pass per Item
for test in ["smoke", "reset", "burst"]:
print(test) # runs three times, once per namesmoke
reset
burstRead the first line as a sentence: for each test in this list, do the following. The indented block is the body, and it runs once for every item the collection contains — three items, three passes.
Nothing in that program mentions 3. Change the list and the loop follows it:
tests = ["smoke", "reset", "burst", "cdc", "power"]
for test in tests:
print(test)smoke
reset
burst
cdc
powerFive items, five passes, and the program was not edited — only the data.
3. What One Pass Actually Does
The sentence version is enough to use a loop. The mechanism is what lets you debug one.
For each item in the collection, in order, Python does four things:
Step 2 is the one to hold on to. Binding the loop variable is an ordinary assignment, the same operation Chapter 2.3 described: a name is pointed at a value. It is not a special kind of link, and it does not connect the name to the collection in any lasting way.
4. The Loop Variable Is an Ordinary Name
Two consequences follow immediately, and both are testable.
First, the loop variable still exists after the loop, holding the item from the final pass:
for test in ["smoke", "reset", "burst"]:
print(test)
print("after the loop, test is:", test)smoke
reset
burst
after the loop, test is: burstNothing cleans it up. The last assignment was test = "burst", and that is what the name still refers to. Chapter 10.8 uses this deliberately; Chapter 10.12 shows where relying on it goes wrong.
Second, you may name it anything. There is nothing magic about test:
for anything_at_all in ["smoke", "reset"]:
print(anything_at_all)smoke
resetName it for what one item is. for test in tests: and for line in lines: read as English; for x in tests: makes every later line harder.
5. Re-pointed, Not a Window
Here is the experiment that settles the mental model. Assign to the loop variable inside the loop, then look at both the list and the name:
tests = ["smoke", "reset"]
for test in tests:
test = test.upper()
print(tests)
print(test)['smoke', 'reset']
RESETThe list is unchanged. The name now holds "RESET".
If the loop variable were a window onto the collection — a slot you could write through — the list would read ['SMOKE', 'RESET']. It does not. test = test.upper() did what every assignment does: it built a new string and pointed the name test at it, leaving the list exactly as it was. On the next pass Python re-pointed test at the next item, discarding that rebinding entirely.
6. The Body Is a Block
Nothing new is needed to read a loop body, because Chapter 9.2 already settled it. The indentation decides what belongs to the loop:
tests = ["smoke", "reset"]
for test in tests:
print("starting", test)
print("finished", test)
print("all tests done")starting smoke
finished smoke
starting reset
finished reset
all tests doneBoth indented lines are inside the body, so both run on every pass — you see starting and finished twice each, interleaved per test rather than grouped. The unindented line is after the loop and runs once, when the loop is over.
Move one line out and the meaning changes exactly as it did for an if:
tests = ["smoke", "reset"]
for test in tests:
print("starting", test)
print("finished", test)starting smoke
starting reset
finished resetfinished now runs once, after the loop, with test holding the last item — which is Section 4's fact, arriving as a bug.
7. The Collection Is Not Consumed
Looping over a list does not empty it or alter it in any way:
tests = ["smoke", "reset"]
for test in tests:
print(test)
print(tests)
print(len(tests))
for test in tests:
print("second pass:", test)smoke
reset
['smoke', 'reset']
2
second pass: smoke
second pass: resetThe list is intact after the first loop and can be looped over again. A for loop reads; it does not take.
8. An Empty Collection Runs the Body Zero Times
There is no special case to handle:
failed = []
for test in failed:
print("investigate", test)
print("done")doneZero items, zero passes, no error. This is worth knowing precisely because it is so often guarded against unnecessarily — if failed: before a loop over failed adds nothing, since the loop already does nothing when the list is empty.
What does follow is that the loop variable may never be created at all:
failed = []
for test in failed:
print(test)
print(test)NameError: name 'test' is not definedThe body never ran, so test was never bound. Code that reads the loop variable after a loop is relying on at least one pass having happened — which Chapter 10.12 returns to.
9. Predict, Run, Observe
Four programs. Predict each before reading the output.
# A
for n in ["a", "b"]:
print(n)
print(n)
# B
items = [1, 2, 3]
for item in items:
item = item * 10
print(items)
# C
for ch in "axi":
print(ch)
# D
counted = 0
for test in ["smoke", "reset", "burst"]:
counted = counted + 1
print("passes:", counted)The verified output:
a
b
b
[1, 2, 3]
a
x
i
passes: 3A — two passes, then the name survives holding "b".
B — [1, 2, 3], unchanged. Section 5's experiment with numbers instead of strings; the result is the same because the reason is the same.
C — a string is a collection of characters, so three passes. Chapter 10.3 covers what each collection type hands you.
D — the body ran once per item, so counted reached 3. That is the whole of Chapter 10.10 in four lines.
10. Exercises
Exercise 1 — rewrite without indices. Replace the three print(tests[...]) lines from Section 1 with a loop, then add two tests to the list and confirm the program needed no other change.
Exercise 2 — name the variable. For each collection, give a good loop-variable name and say why in a few words: (a) seeds (b) lines (c) widths (a dictionary of signal name to bit width) (d) failed_tests.
Exercise 3 — the window experiment. Run Section 5's program, then write down in two sentences what ['smoke', 'reset'] proves and what RESET proves. Which of the two would have surprised you before this chapter?
Exercise 4 — inside or after. For the program below, predict the output, then move print(total) four spaces to the right and predict again. Verify both.
total = 0
for n in [1, 2, 3]:
total = total + n
print(total)Exercise 5 — the empty case. Write a loop over an empty list that would print something on every pass, and confirm it prints nothing and raises nothing. Then add a line after the loop that uses the loop variable, and record the exact error.
Exercise 6 — count the passes. Without running it, say how many times the body runs for each: (a) for x in [1, 2, 3] (b) for ch in "ab" (c) for x in [] (d) for x in ["only"]. Verify all four.
Exercise 7 — two statements per pass. Write a loop over three test names that prints two lines per test, then predict how many lines appear in total and verify.
Exercise 8 — explain the model. In three sentences and without using the word "loop", describe what happens to the name test during for test in tests:. Use the word "assignment" at least once.
11. Summary
- A
forloop runs its body once for each item in a collection, in order. - The loop is driven by the collection, not by a count — change the data and the loop follows it with no edit.
- One pass does four things: take the next item, bind the loop variable to it, run the body, go back for the next.
- Binding the loop variable is an ordinary assignment. It is not a link to the collection.
- The loop variable is a normal name: you choose it, and it survives the loop holding the last item.
- Assigning to it does not change the collection —
test = test.upper()leaves the list untouched and is discarded on the next pass. - The body is a block, exactly as in Chapter 9.2; the column decides what runs per pass and what runs once.
- Looping does not consume the collection. It can be looped over again immediately.
- An empty collection runs the body zero times, raises nothing, and leaves the loop variable undefined.
The next chapter puts this one loop shape to work on the three things an engineer actually iterates over.
Where this fits
Part of the Python curriculum.
