This lesson separates assignment from equality once and for all, introduces updating a variable and the initialization it requires, gives the formal flow of execution for a while statement, and asks what it takes to prove that a loop terminates.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 7 — Iteration
§7.1-7.3, pp. 63-65
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 63-65 — the pages these objectives are drawn from
Warm-up
This is the third mechanism for doing something repeatedly. Name the other two.
Discussion prompt
You have already met two ways of running a block of statements repeatedly, in two different chapters. Name both, and say one thing each is good at.
Hint: One was in a case study; the other was a function calling itself.
Answer:
The for loop, in chapter 4, which repeated a fixed number of times — good when you know the count in advance.
Recursion, in chapter 5, which repeated by calling itself — good when the problem has a naturally recursive definition.
This chapter adds a third, the while statement, which repeats until a condition becomes false. That is the one you reach for when you do not know in advance how many times you will need.
Concept
A while loop repeats its body as long as a condition is true. For that to end, the body must change something the condition depends on — which is why this chapter starts by looking carefully at what it means to change a variable.
iteration — The repeated execution of a set of statements, using either recursion or a loop.
The body of the loop should change the value of one or more variables so that the condition becomes false eventually and the loop terminates. Otherwise the loop will repeat forever, which is called an infinite loop.
Figure (svg): A flow chart showing a condition being tested, the body running, and the flow looping back to the condition
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 63-65
Section
Section 1
Concept
It is legal to make more than one assignment to the same variable. A new assignment makes an existing variable refer to a new value, and stop referring to the old one. Because Python uses the equal sign for assignment, it is tempting to read a = b as a claim that a and b are equal — but that interpretation is wrong, for two separate reasons.
>>> x = 5
>>> x
5
>>> x = 7
>>> x
7| Line | What happens | State after |
|---|---|---|
| x = 5 | creates x, pointing at 5 | x -> 5 |
| x = 7 | repoints the SAME name | x -> 7 |
| the 5 | no longer referred to by x | gone |
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 63-64
Picture it
The name stays; what it points at changes. This is the state diagram from lesson 2a, one step later.
Figure (svg): A state diagram showing the name x with an arrow that has moved from five to seven
Nothing about the 5 was modified. The name simply stopped pointing at it, which is a different thing and matters enormously in chapter 10.
Worked example
The book's own example, and it is the one that catches people.
>>> a = 5
>>> b = a
>>> a = 3
>>> b
5| Line | What happens | State after |
|---|---|---|
| a = 5 | a points at 5 | a -> 5 |
| b = a | b points at whatever a points at | a -> 5, b -> 5 |
| a = 3 | a is repointed; b is not mentioned | a -> 3, b -> 5 |
Make them equal.
Why: After the second line, a and b are now equal — both refer to 5.
Change one of them.
Why: The third line changes the value of a but does not change the value of b.
Notice why.
Why: b = a copied the ARROW, not a link to a. Once b had its own arrow, later changes to a had nothing to do with it.
Figure (svg): The state of the program after each line of Worked example two variables that stop being equal, drawn as a ladder with one rung per traced line
b is still 5. Assignment made them equal for a moment, and nothing kept them that way — which is the second of the two reasons assignment is not equality.
Verify: Compare with the mathematical reading.
Why: In mathematics, if a = b then changing a would change b, because the equation is a permanent claim. Here it does not, which is a direct demonstration that the equals sign is doing something else. Lesson 2a's invariant-watch probe showed the same thing and this is the book stating it as a principle.
Prediction
One assignment copies an arrow; the other moves one.
a = 10
b = a
a = 20
print(b)| Line | What happens | State |
|---|---|---|
| b = a | b points at what a points at | both -> 10 |
| a = 20 | a is repointed | a -> 20, b -> 10 |
| print(b) | b was never mentioned again | 10 |
Predict first
What does this print?
Correct: 10 — b was given its own arrow to 10, and repointing a afterwards had nothing to do with it.
Why: The second line copied the value, not a connection to a. This is the book's own example of assignment's impermanence: an assignment statement can make two variables equal, but they do not have to stay that way. It becomes a much bigger deal in chapter 10, where the value being pointed at is a list that can itself be changed.
Worked example
The asymmetry, made concrete. Try it both ways round.
>>> a = 7
>>> 7 = a
SyntaxError: cannot assign to literal| Line | What is on the left | Result |
|---|---|---|
| a = 7 | a name on the left | legal: the name is repointed |
| 7 = a | a literal on the left | SyntaxError |
| why | there is nowhere to put the value | 7 is not a name |
Recall lesson 3a's exception.
Why: Almost anywhere you can put a value you can put an expression, with one exception: the left side of an assignment has to be a variable name.
Apply it here.
Why: 7 is a literal, not a name, so there is nothing to repoint. Python reports that it cannot assign to a literal.
Draw the conclusion.
Why: In mathematics if a = 7 then 7 = a. In Python one is legal and the other is not, which means the operator is not symmetric and therefore is not equality.
Figure (svg): Two columns contrasting the asymmetry of assignment with the symmetry of the equality operator
A SyntaxError. The two sides of an assignment mean different things — one names a place and the other produces a value — so swapping them is not a rephrasing but an error.
Verify: Check that == is symmetric, as equality should be.
Why: Both a == 7 and 7 == a are legal and give the same answer. The operator that really does mean equality behaves symmetrically, which is exactly the contrast the argument needs.
Trap
A variable called value is reassigned nine times in twenty lines, each time to something conceptually different.
Reuse a name because it is convenient
Why: Reassignment is legal, and inventing new names feels wasteful.
Now no reader can say what value holds at any given line without tracing the whole function, and the name documents nothing.
Reassign deliberately, and mostly to update rather than to repurpose.
Reassign when the new value is the same THING, changed
Why: A running total, a current estimate, a loop counter. The name still describes what it holds.
Use a new name when the new value is a different thing
Why: Names are free, and one name per concept is what makes a function readable.
The book puts it as a caution rather than a prohibition: reassigning variables is often useful, but you should use it with caution — if the values of variables change frequently, it can make the code difficult to read and debug.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
Survives elimination: C
Why: C is the lie, and it is the misreading the whole section exists to correct. b = a gives b its own reference to the current value; it does not link b to a. Changing a afterwards repoints a alone. If the assignment created a lasting link, the impermanence argument would fail and the equals sign really would mean equality.
Invariant
Step through and watch which arrows move.
Step through it
After the fourth line, how many names exist and what does each refer to?
Two names: x refers to 9 and y to 7. Each assignment moved exactly one arrow — the one belonging to the name on its left — which is the whole mechanism.
Explain it to yourself
The book gives two independent reasons. Make sure you can give both.
Discussion prompt
State the two reasons the book gives for why a = b is not a claim of equality, and explain why one of them alone would not be enough.
Hint: One is about direction; the other is about time.
Answer:
First, equality is symmetric and assignment is not: a = 7 is legal and 7 = a is a syntax error.
Second, equality is permanent and assignment is not: a = b can make two variables equal and they need not stay that way.
Neither alone settles it. A language could have an asymmetric equality operator, or a symmetric assignment that was still temporary — so the two arguments rule out different alternatives. Together they establish that the operator is doing something other than asserting a relationship, which is exactly what lesson 2a's becomes reading captured.
Section
Section 2
Concept
A common kind of reassignment is an update, where the new value of the variable depends on the old. It is the mechanism every loop is built on.
update — An assignment in which the new value of a variable depends on its old value.
>>> x = 0
>>> x = x + 1
>>> x
1| Line | What happens | Note |
|---|---|---|
| x = 0 | initialization | x -> 0 |
| x = x + 1 | read x, add one, repoint x | x -> 1 |
| without the first line | there is no old value to read | NameError |
This means: get the current value of x, add one, and then update x with the new value. Adding 1 is called an increment; subtracting 1 is called a decrement.
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 64-64
Picture it
Three steps in one line, and they happen in this order.
Figure (svg): A ladder showing x equals x plus one being evaluated as the old value plus one and then assigned
This is lesson 2a's read-then-write rule, and it is why an update needs an initialization: the read happens first, and there must be something to read.
Worked example
Predict the error before advancing. The reason for it is the read-then-write rule.
>>> x = x + 1
NameError: name 'x' is not defined| Step | What happens | Result |
|---|---|---|
| evaluate x + 1 | look up x | there is no x |
| the error | NameError | before any assignment happens |
| the fix | initialize x first | x = 0 |
Recall the order.
Why: Python evaluates the right side before it assigns a value to x — which is exactly lesson 2a's rule.
See why that causes the error.
Why: Evaluating x + 1 requires looking up x, and x does not exist yet. The failure happens before the assignment is even attempted.
State the fix.
Why: Before you can update a variable, you have to initialize it, usually with a simple assignment.
Figure (svg): The state of the program after each line of Worked example updating a variable that does not exist, drawn as a ladder with one rung per traced line
A NameError, because the right-hand side is evaluated first and there is nothing to read. The fix is an initialization: assign a starting value before the first update.
Verify: Check that the error message names x rather than the addition.
Why: It says name 'x' is not defined, which points at the lookup rather than at the arithmetic. The message is telling you which of the three steps failed, and it is the first one.
Prediction
The initialization is in the wrong place.
n = 1
while n <= 3:
total = 0
total = total + n
n = n + 1
print(total)| Pass | What happens | total after |
|---|---|---|
| pass 1 | total reset to 0, then 0 + 1 | total 1 |
| pass 2 | total reset to 0, then 0 + 2 | total 2 |
| pass 3 | total reset to 0, then 0 + 3 | total 3 |
Predict first
What does this print?
Correct: 3 — total is reset to zero at the start of every pass, so it only ever holds the last value added.
Why: The initialization is inside the body, so it runs once per pass rather than once in total. The accumulation is destroyed each time and only the final addition survives. Note the characteristic symptom: the answer equals the last item rather than the sum, which is the fingerprint of this bug.
Worked example
The same loop, two starting values. Both are correct programs.
total = 0
n = 1
while n <= 4:
total = total + n
n = n + 1| Stage | What happens | State |
|---|---|---|
| start | total = 0, n = 1 | initialized |
| pass 1 | total = 0 + 1 = 1; n = 2 | total 1 |
| pass 2 | total = 1 + 2 = 3; n = 3 | total 3 |
| pass 3 | total = 3 + 3 = 6; n = 4 | total 6 |
| pass 4 | total = 6 + 4 = 10; n = 5 | condition now false |
Notice there are two variables being updated.
Why: total accumulates the sum and n counts the passes. Both are initialized before the loop and both are updated inside it.
Notice what each initialization means.
Why: total starts at 0 because an empty sum is zero. n starts at 1 because that is the first number to add.
Notice which update makes the loop end.
Why: The update to n. Without it the condition would never become false, however correct the total was.
Figure (svg): A state diagram showing total and n after each pass of the loop, ending with total ten and n five
10, the sum of 1 to 4. Two variables are updated on every pass: one accumulates the answer and one drives the loop toward termination.
Verify: Change the initialization of total to 100 and predict the result.
Why: 110 — the loop adds the same 10 to a different starting point. That the initialization changes the answer without changing the loop is what makes it a genuine part of the algorithm rather than boilerplate.
Trap
A student puts total = 0 inside the loop body rather than before it.
Group all the setup with the code that uses it
Why: It looks tidier to have the variable created next to where it is updated.
Now total is reset to zero on every pass, so it never accumulates anything and ends up holding only the last value added.
Initialize before the loop; update inside it.
Ask how many times each line should run
Why: The initialization should run once. Anything inside the body runs once per pass.
Check the answer against a case you can compute
Why: Summing 1 to 4 should give 10. Getting 4 is the signature of an initialization inside the loop.
The symptom is characteristic: a total that always equals the last item, or a counter that is always 1. Both mean an initialization that is being re-run.
Discrimination
Ask how many times each line should run.
Sort into buckets
For a loop that sums the numbers 1 to n, where does each line belong?
Faded example
The update needs something to read.
Fill in the blanks
count = 0
while n < 5:
count = count + 1
n = n + 1
Why: Without the initialization, the first pass evaluates count + 1, looks up count, and raises a NameError — because Python evaluates the right side before assigning. Note that n also needs initializing, which the template assumes has happened above: a loop with two updated variables needs two initializations, and forgetting either one produces the same error.
Matching
Four words from this section, used precisely throughout the book.
Match the pairs
Why: The four are nested: an initialization is a plain assignment, an update is a reassignment that reads the old value, and an increment is a particular update. Being precise about them matters because the error messages differ — forgetting the initialization gives a NameError, while getting the update wrong gives a silently wrong answer.
Section
Section 3
Concept
Computers are often used to automate repetitive tasks, and repeating identical or similar tasks without making errors is something computers do well and people do poorly. The while statement is Python's way of expressing that directly.
def countdown(n):
while n > 0:
print(n)
n = n - 1
print('Blastoff!')| Line | Its role | Note |
|---|---|---|
| n > 0 | the condition, checked before each pass | true or false |
| print(n) | the body | displays the current n |
| n = n - 1 | the update | moves toward the condition failing |
| print('Blastoff!') | not indented | runs after the loop ends |
You can almost read the while statement as if it were English: while n is greater than 0, display the value of n and then decrement n. When you get to 0, display the word Blastoff.
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 64-65
Picture it
Chapter 5 wrote this with recursion. Compare the two.
Figure (svg): Two columns comparing the recursive countdown with the while loop version
That is the essential difference between the two mechanisms, and it explains why deep recursion runs out of stack while a loop does not.
Worked example
Apply the three steps literally, one pass at a time.
def countdown(n):
while n > 0:
print(n)
n = n - 1
print('Blastoff!')| Value of n at the test | The condition | What happens |
|---|---|---|
| n = 3 | 3 > 0 is true | print 3, n becomes 2 |
| n = 2 | 2 > 0 is true | print 2, n becomes 1 |
| n = 1 | 1 > 0 is true | print 1, n becomes 0 |
| n = 0 | 0 > 0 is false | leave the loop, print Blastoff |
Test before every pass, including the first.
Why: Step 1 of the flow of execution is to determine whether the condition is true or false, and it happens before the body runs at all.
Run the body, then loop back.
Why: If the condition is true, run the body and then go back to step 1. The test happens again before the next pass.
Leave when the condition fails.
Why: If false, exit the while statement and continue execution at the next statement — which here is the un-indented print.
Figure (svg): A flow chart showing the countdown loop testing n, printing, decrementing, and looping back until the condition fails
3, 2, 1, Blastoff — the same output as the recursive version, produced by four tests and three passes through the body.
Verify: Count the tests and the passes.
Why: Four tests and three passes: the condition is checked one more time than the body runs, because the last check is the one that ends the loop. That off-by-one is not an error — it is the shape of a while loop, and expecting it prevents a whole family of confusions.
Prediction
The condition is tested before each pass, including the first.
n = 3
while n > 0:
print(n)
n = n - 1| Test | Result | Consequence |
|---|---|---|
| test with n = 3 | true | pass 1 |
| test with n = 2 | true | pass 2 |
| test with n = 1 | true | pass 3 |
| test with n = 0 | false | leave |
Predict first
How many times does the body run, and how many times is the condition tested?
Correct: Three passes and four tests — the condition is checked once more than the body runs, because the final check is the one that ends the loop.
Why: Step 1 of the flow of execution happens before every pass and once more at the end. That extra test is easy to forget and it matters when the condition has a side effect or is expensive to evaluate. It is also why a loop can run zero times: the very first test may already be false.
Worked example
The condition is tested before the first pass. Work out what that allows.
>>> countdown(0)
Blastoff!
>>> countdown(-5)
Blastoff!| Argument | The first test | Passes |
|---|---|---|
| n = 0 | 0 > 0 is false immediately | the body never runs |
| n = -5 | -5 > 0 is false immediately | the body never runs |
| output | just Blastoff | zero passes |
Apply step 1 before anything else.
Why: The condition is determined first, so a loop whose condition is false at the start runs its body zero times.
Notice this is not an error.
Why: Zero passes is a perfectly ordinary outcome, and it is often the right one — a loop over an empty collection should do nothing.
Connect it to the termination proof.
Why: In the case of countdown we can prove the loop terminates: if n is zero or negative, the loop never runs.
Figure (svg): The state of the program after each line of Worked example a loop whose body never runs, drawn as a ladder with one rung per traced line
Both print only Blastoff. Because the condition is tested before the first pass, a while loop can run its body zero times — which is exactly what makes countdown safe for negative arguments.
Verify: Compare with the recursive version from lesson 5c.
Why: That version also handled negatives, because its base case tested n <= 0 rather than n == 0. Both mechanisms need the same care about the boundary, and both get it right here for the same reason: the check is an inequality rather than an equality.
Trap
A loop counts down but the programmer forgets the line that decrements the counter.
Write the body for what it should DO
Why: Printing the value is the visible work; the decrement is bookkeeping and easy to omit.
The condition is checked, found true, checked again, found true — forever. The program prints the same number until it is killed.
The body must change something the condition depends on.
After writing the body, find the variable in the condition
Why: Then check that the body assigns to it, in a direction that moves toward failure.
Prove it if you can
Why: For countdown: if n is zero or negative the loop never runs, and otherwise n gets smaller each time, so eventually it reaches 0.
The book states the requirement directly: the body of the loop should change the value of one or more variables so that the condition becomes false eventually and the loop terminates. Otherwise the loop will repeat forever, which is called an infinite loop.
Ranking
The book states them formally. Put them in order.
Put in order
Why: Test first, then the false case, then the true case, then loop back. Putting the test anywhere but first would change the meaning: a loop whose body ran before the first test would always run at least once, which is a different construct that some languages provide separately. Python's while always tests first.
Comparison
Fill the blanks. You have now met all three.
Comparison matrix
| Mechanism | How it repeats | When to reach for it |
|---|---|---|
| for loop | once per item in a range or sequence | when you know how many times in advance |
| recursion | a function calls itself, one frame per level | when the problem has a recursive definition |
| while loop | until a condition becomes false | when you do not know the number of passes in advance |
The bottom row is what this chapter adds. Newton's method in the next lesson is the clearest case: you cannot know in advance how many improvements it will take.
Socratic
Some languages offer a loop that tests afterwards. Compare them.
Discussion prompt
Python's while tests the condition before running the body, so the body may run zero times. What would be different if it tested afterwards, and which behaviour is more often what you want?
Hint: Think about looping over something that might be empty.
Answer:
Testing afterwards would guarantee at least one pass, so a loop over an empty collection would process a nonexistent item — which is nearly always wrong.
Testing first handles the empty case correctly with no extra code, and that case is extremely common: an empty list, a file with no lines, a countdown from zero.
Which is why countdown(0) prints only Blastoff and needs no special handling. The zero-pass case being natural rather than exceptional is a real design benefit, and it is why test-first is the default in most languages that offer both.
Section
Section 4
Concept
In the case of countdown we can prove that the loop terminates: if n is zero or negative the loop never runs, and otherwise n gets smaller each time through the loop, so eventually we have to get to 0. For some other loops it is not so easy to tell.
def sequence(n):
while n != 1:
print(n)
if n % 2 == 0:
n = n / 2
else:
n = n*3 + 1| Case | What the body does | Effect on n |
|---|---|---|
| n even | halve it | n gets smaller |
| n odd | triple it and add one | n gets BIGGER |
| termination | n sometimes increases and sometimes decreases | no obvious proof |
The condition is n != 1, so the loop continues until n is 1. Since n sometimes increases and sometimes decreases, there is no obvious proof that n will ever reach 1. For a starting value of 3 the sequence is 3, 10, 5, 16, 8, 4, 2, 1.
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 65-65
Picture it
Starting from 3, the value more than triples before it starts shrinking.
Figure (svg): The Collatz sequence starting at three drawn as a row of boxes: three, ten, five, sixteen, eight, four, two, one
The tail of that sequence — 16, 8, 4, 2, 1 — is a power of two, and once you reach one the rest is guaranteed: it will be even every time until it reaches 1.
Worked example
A real proof, in two cases, and it is short.
def countdown(n):
while n > 0:
print(n)
n = n - 1| Case | Argument | Conclusion |
|---|---|---|
| case 1: n <= 0 | the condition is false at once | zero passes; terminates |
| case 2: n > 0 | n decreases by exactly 1 each pass | must reach 0 |
| conclusion | both cases terminate | proved |
Split into cases that cover every input.
Why: Either n starts at zero or below, or it starts above zero. Nothing else is possible.
Handle the easy case.
Why: If n is zero or negative, the loop never runs — so it certainly terminates.
Handle the other.
Why: Otherwise n gets smaller each time through the loop, by a fixed amount, so eventually we have to get to 0.
Figure (svg): The state of the program after each line of Worked example proving countdown terminates, drawn as a ladder with one rung per traced line
The loop terminates for every integer n. The proof is two cases and one observation: the value decreases by a fixed amount toward a fixed target.
Verify: Check whether the proof survives if n starts as a float.
Why: For n = 2.5 the values go 1.5, 0.5, -0.5 — and the condition n > 0 does become false, so it still terminates. The proof survives because the condition is an inequality, unlike lesson 6c's factorial, whose == 0 test a float could step past. Inequalities are more robust than equalities for exactly this reason.
Prediction
Apply the rule: halve if even, triple and add one if odd.
Predict first
Starting from 6, what are the next three values in the sequence?
Correct: 3, 10, 5 — six is even so it halves to 3, three is odd so it becomes 10, and ten is even so it halves to 5.
Why: Notice the increase at the second step: 3 becomes 10, more than tripling. That is exactly why the countdown termination proof does not apply — there is no quantity that decreases on every pass. From 6 the full sequence is 6, 3, 10, 5, 16, 8, 4, 2, 1, which takes eight passes to reach a value that started smaller.
Worked example
The book includes it deliberately. Work out where the argument breaks down.
def sequence(n):
while n != 1:
print(n)
if n % 2 == 0:
n = n / 2
else:
n = n*3 + 1| Starting value | The sequence | Verdict |
|---|---|---|
| start 3 | 3, 10, 5, 16, 8, 4, 2, 1 | terminates after 7 passes |
| start 16 | 16, 8, 4, 2, 1 | a power of two: always halved |
| start anything | nobody has proved it terminates | an open problem |
Try the countdown proof and watch it fail.
Why: That proof relied on the value decreasing every pass. Here it increases whenever n is odd, so there is no decreasing quantity to argue from.
Find a case that can be proved.
Why: If the starting value is a power of two, n will be even every time through the loop until it reaches 1. That is a real proof for a restricted set of inputs.
State the open question.
Why: The hard question is whether we can prove that this program terminates for all positive values of n. So far, no one has been able to prove it or disprove it.
Figure (svg): Two columns comparing a loop whose termination can be proved with one whose termination is an open problem
It terminates for every value anybody has tried, and no proof exists for all of them. The countdown argument fails because n does not decrease monotonically.
Verify: Check the claim about powers of two on 16.
Why: 16, 8, 4, 2, 1 — even at every step, halved each time, reaching 1 in four passes. That special case is provable by exactly the countdown argument, which shows what the general case is missing: a quantity that always moves the same way.
Trap
A loop is tested on a hundred inputs, terminates on all of them, and is declared safe.
Treat evidence as proof
Why: A hundred successes is genuinely good evidence, and for most loops it would be enough.
The Collatz sequence terminates for every value ever tested — billions of them — and remains unproved. Evidence and proof are different things, and a loop can hide an unbounded case.
Look for a quantity that changes monotonically toward the condition failing.
Identify what the condition depends on
Why: Then ask whether the body always moves it in one direction.
If it does, you have a proof; if it does not, you have a risk
Why: Which may be acceptable, but it should be known rather than assumed.
Most loops you write will be the easy kind, with a counter that only ever increases or decreases. Noticing when a loop is NOT of that kind is the skill, and the Collatz example exists to make that distinction memorable.
Discrimination
Look for a quantity that always moves toward the condition failing.
Sort into buckets
For each loop, is termination easy to prove?
Counterexample
countdown's proof relied on n decreasing. Test whether that is enough on its own.
Discussion prompt
Construct a loop in which the variable decreases on every single pass and which nonetheless never terminates. What does your example show about the countdown proof?
Hint: It must decrease by a fixed amount, not merely decrease.
Answer:
while x > 0: x = x / 2 — starting from 1, x becomes 0.5, 0.25, 0.125 and so on. It decreases every pass and never reaches zero.
So decreasing is not enough. The countdown proof relied on decreasing by a FIXED amount toward the target, which guarantees arrival in a finite number of steps.
This matters in practice: loops that halve a value or shrink an interval are extremely common, and they terminate only because they test an inequality against a tolerance rather than waiting for an exact value. The next lesson's Newton's method is exactly such a loop, and its termination test is the subject of a whole section.
Real world
Most of the time you can eyeball it. Sometimes you cannot.
Discussion prompt
Think of a real process that repeats until something is true — searching, refining, retrying. What would happen if the condition were never met, and how do real systems protect against that?
Hint: Consider a program retrying a network request.
Answer:
It would repeat forever, consuming resources and never reporting a problem. A retry loop with no limit is the classic case, and it turns a temporary failure into a permanent hang.
Real systems add a second condition: retry until it succeeds OR until a limit is reached. That converts a loop whose termination depends on the world into one whose termination is provable.
Which is the general technique when you cannot prove termination from the logic: add a counter and a maximum. It is not elegant, and it turns an unbounded risk into a bounded one — which is exactly the trade Python itself makes with its recursion limit.
Section
Section 5
Concept
Every correct while loop has the same four parts, and the failures of this lesson are each a missing one: initialize, test, work, update.
total = 0 # initialize
n = 1 # initialize
while n <= 10: # test
total = total + n # work
n = n + 1 # update
print(total)| Part | Where and how often | Why it is needed |
|---|---|---|
| initialize | before the loop, once | gives the update something to read |
| test | before every pass | decides whether to continue |
| work | inside, once per pass | what the loop is for |
| update | inside, once per pass | moves toward the test failing |
Leaving out the initialization gives a NameError. Leaving out the update gives an infinite loop. Putting the initialization inside gives a silently wrong answer. Each failure has its own characteristic symptom.
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 64-65
Picture it
Each omission has its own signature.
Figure (svg): Two columns listing the four parts of a loop against the failure caused by omitting each
Diagnosing by symptom is faster than reading, which is the same principle as the error taxonomy in lesson 2b.
Worked example
The book's exercise: take a recursive function from chapter 5 and make it iterative.
# recursive, from lesson 5c:
# def print_n(s, n):
# if n <= 0: return
# print(s)
# print_n(s, n-1)
def print_n(s, n):
while n > 0:
print(s)
n = n - 1| Recursive part | What it was | What it becomes |
|---|---|---|
| the base case | if n <= 0: return | becomes the loop condition n > 0 |
| the work | print(s) | unchanged |
| the recursive call | print_n(s, n-1) | becomes the update n = n - 1 |
Turn the base case into the loop condition, reversed.
Why: The recursion stops when n <= 0, so the loop continues while n > 0. A base case and a loop condition are opposites of each other.
Keep the work unchanged.
Why: print(s) does the same job in both versions.
Turn the recursive call into an update.
Why: Calling with n-1 becomes assigning n - 1 to n. Both mean go round again with a smaller n.
Figure (svg): Two columns mapping the parts of a recursive function onto the parts of the equivalent loop
A three-line loop that does what the four-line recursion did. The base case became the negated condition, and the recursive call became the update.
Verify: Check both versions on n = 0 and n = 3.
Why: Both print nothing for 0 and three lines for 3. The translation is only correct if it agrees on the boundary as well as the ordinary case, and n = 0 is the boundary here — which the loop handles by testing before the first pass.
Error analysis
Mark what is wrong and say what the symptom would be.
Annotate
This is the commonest infinite loop there is, and the check that prevents it takes two seconds: name the variable in the condition, then find an assignment to it in the body.
Worked example
Three broken loops. Name the missing part from the symptom alone.
# symptom A: NameError on the first pass
# symptom B: the program hangs, printing the same value
# symptom C: the total equals the last item, not the sum| Symptom | What it means | The missing part |
|---|---|---|
| A | the update read a variable that does not exist | missing initialization |
| B | the condition never becomes false | missing update |
| C | the accumulator is reset every pass | initialization inside the loop |
Read symptom A.
Why: A NameError on the first pass means the right-hand side of an update found nothing to read, which is a missing initialization.
Read symptom B.
Why: Repeating forever with an unchanging value means the body is not changing what the condition depends on.
Read symptom C.
Why: A total equal to the last item means the accumulator was reset each pass, which is an initialization in the wrong place.
Figure (svg): The state of the program after each line of Worked example diagnosing a loop by its symptom, drawn as a ladder with one rung per traced line
Three symptoms, three different missing parts, all diagnosable without reading the code. Each failure mode has a signature.
Verify: Check that the three symptoms really are distinguishable.
Why: One is an error, one is a hang, and one is a wrong answer — which are the three categories from lesson 2b's error taxonomy. A loop can fail in all three ways, and knowing which you have narrows the search before you read a line.
Trap
A loop tests one variable and the body updates a different one, usually because of a copy-and-paste.
Check that the body contains an update, without checking which variable
Why: There is an assignment in there, and it looks like the bookkeeping line.
The condition's variable never changes, so the loop runs forever — while some other variable counts diligently upward.
Match the update to the condition explicitly.
Name the variable the condition tests
Why: Then find an assignment to THAT variable in the body.
Check the direction as well as the variable
Why: Incrementing a variable in a condition that requires it to decrease is as bad as not updating it.
This is a two-second check and it catches the commonest cause of an infinite loop. It is worth doing every time you write a while statement, before you run it.
Prediction
All four parts are present. Trace it.
n = 5
result = 1
while n > 1:
result = result * n
n = n - 1
print(result)| Pass | What happens | result after |
|---|---|---|
| n = 5 | result = 1 * 5 | 5, n becomes 4 |
| n = 4 | result = 5 * 4 | 20, n becomes 3 |
| n = 3 | result = 20 * 3 | 60, n becomes 2 |
| n = 2 | result = 60 * 2 | 120, n becomes 1 |
Predict first
What does this print?
Correct: 120 — this is the factorial of 5, computed iteratively.
Why: The loop multiplies result by 5, 4, 3 and 2, which is 120. Note that it stops at n > 1 rather than n > 0, because multiplying by 1 would change nothing — a small optimisation that also happens to make the loop match the mathematical definition. Compare this with the recursive factorial from lesson 6b: same answer, one frame instead of six.
Sorting
Diagnose from the symptom.
Sort into buckets
For each symptom, which of the four parts is missing or misplaced?
Explain it
The commonest question a beginner asks about loops.
Discussion prompt
A classmate's while loop hangs. Without seeing the code, give them the one check to run first, and explain why it catches most cases.
Hint: It involves naming one variable.
Answer:
Say: name the variable in the condition, then look for an assignment to that exact variable in the body. If there is none, that is your bug.
It catches most cases because the requirement is precise — the body must change the value of one or more variables so that the condition becomes false eventually — and the two common failures are no update at all and an update to the wrong variable.
Add the second half of the check: even when there is an update, confirm it moves in the right direction. Incrementing a counter in a loop that runs while the counter is below a limit is fine; incrementing it in a loop that runs while it is above one is an infinite loop with an update in it.
Comparison
Fill the blanks. Both repeat; they differ in where the state lives.
Comparison matrix
| Question | Recursion | while loop |
|---|---|---|
| Where is the current value kept? | in each frame's parameter | in a variable that is updated |
| How many frames? | one per level | one, always |
| What stops it? | reaching the base case | the condition becoming false |
| How does it fail? | infinite recursion, then a RecursionError | an infinite loop, which never stops on its own |
The last row is worth noticing: Python stops an infinite recursion at a thousand frames, and an infinite loop it will run forever. The loop's failure mode is quieter and worse.
Pattern
Six steps, and the last two are checks rather than code.
Step 5 takes two seconds and prevents the most common failure. Step 6 catches the off-by-one, which is the most common wrong answer.
Python documentation — An Informal Introduction to Python An Informal Introduction to Python
Check
One assignment copies a value; it does not create a link.
a = 5
b = a
a = 3| Line | What happens | State |
|---|---|---|
| b = a | b gets its own reference to 5 | a -> 5, b -> 5 |
| a = 3 | a is repointed | a -> 3, b -> 5 |
| b | never mentioned again | still 5 |
Check your understanding
What is the value of b?
Answer: B
Why: The second line gave b its own reference to the value 5. The third line repointed a and mentioned b not at all, so b still refers to 5. This is the book's demonstration that assignment is not equality: an assignment statement can make two variables equal, but they do not have to stay that way.
Check
The condition is tested before every pass, including the first.
n = 0
while n > 0:
print(n)
n = n - 1
print('done')| Step | What happens | Output |
|---|---|---|
| first test | 0 > 0 is false | the body never runs |
| exit | continue at the next statement | print('done') |
| output | one line | done |
Check your understanding
What does this print?
Answer: B
Why: The condition is determined before the body runs, and it is false immediately, so the loop runs zero times and execution continues at the next statement. A while loop running its body zero times is a normal and often desirable outcome — it is what makes this countdown safe for zero and negative arguments.
Check
Look for a quantity that moves toward the condition failing.
Check your understanding
Which of these loops is easiest to prove terminates, for any positive integer n?
Answer: B
Why: Floor division by two strictly decreases any n above 1, and it cannot skip past the target because the condition is an inequality. So n must eventually fall to 1 and the loop ends. The proof has the same shape as countdown's: a quantity that always moves one way toward a target it cannot overshoot.
Real world
Loops that repeat until a condition holds are everywhere, and so are the ones that do not stop.
Discussion prompt
Find a real process that repeats until something is true, and identify its equivalent of the four parts: what is initialized, what is tested, what work is done, and what is updated. Then say what would happen if the update were missing.
Hint: Boiling something until it thickens, or searching a shelf.
Answer:
Stirring a sauce until it thickens: the initialization is putting it on the heat, the test is whether it has thickened, the work is stirring, and the update is the heat continuing to act.
Without the update — the heat off — you would stir forever and the test would never pass. That is an infinite loop, and the analogy is exact.
The useful transfer is the diagnostic: when a repeated process is not finishing, ask what is supposed to be changing and check whether it actually is. That question resolves most stuck loops, in code and out of it.
Commit first
Answer, then rate your confidence. This one is about counting.
Predict first
A while loop runs its body three times before the condition becomes false. How many times is the condition evaluated?
Correct: Four — once before each of the three passes, and once more at the end, which is the evaluation that ends the loop.
Why: The flow of execution tests first, every time, and the loop only exits when a test comes back false — so there is always exactly one more test than there are passes. This matters whenever the condition is expensive to evaluate or has a side effect, and it is also why a loop can run zero times: the first test may already be false. If you answered three, you were counting the tests that led to a pass and forgetting the one that did not.
Explain it
The assignment-is-not-equality argument is worth being able to make properly.
Discussion prompt
A classmate who is good at mathematics is uncomfortable with reassignment: if x was 5, how can it now be 7? Give them the two arguments the book gives, and one line of Python that demonstrates each.
Hint: One demonstration is an error; the other is a surprising value.
Answer:
First, asymmetry: a = 7 is legal and 7 = a is a syntax error. Equality would work both ways round; assignment does not, because one side names a place and the other produces a value.
Second, impermanence: after a = 5 and b = a, setting a = 3 leaves b at 5. Equality would be permanent; assignment is a one-time act.
Then give them the reading that resolves it: the equals sign means becomes, and Python has a separate operator, ==, that really does mean equals. Once they have both operators, the discomfort usually goes.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The assignment argument is conceptual and worth settling once, because it comes back in chapter 10 in a much sharper form when the values are lists. Initialization becomes automatic after two or three loops, and its error message is unusually clear. The three-step flow is worth stating out loud, because the extra final test is where off-by-one errors come from. And termination is the one that keeps mattering — most of your loops will be provably finite, and recognising the ones that are not is a genuinely valuable skill.
Connect it up
One page, from memory.
Draw it
Draw a while loop as a flow chart with the three steps labelled, and mark on it where the condition is tested for the last time. Beside it, list the four parts of a working loop and, next to each, the symptom you would see if it were missing or misplaced. Finally, write the two-case termination proof for countdown, and say in one sentence which step of it the Collatz sequence fails.
Recap
Three pages, and the third and last of the book's repetition mechanisms.
| If you remember one thing | It is this |
|---|---|
| From reassignment | b = a copies the arrow. It does not link b to a. |
| From updating | Initialize before the loop. The right side is read before the left is written. |
| From while | The condition is tested one more time than the body runs. |
| From termination | The body must change what the condition depends on, in the right direction. |
| From Collatz | Terminating for every value ever tried is not the same as being proved to terminate. |
The next lesson finishes the chapter with the break statement, an algorithm that improves an estimate until it stops changing, the reason you must not test floats for equality, and a debugging technique that halves the search each time you use it.
Think Python, 2nd edition — Allen B. Downey §7.1-7.3, pp. 63-65 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.