CIS 2020 · Python for Everyone · Module 7 · Study guide
Chapter 4 moves from a decision that runs once to a decision that repeats. This module covers the while loop that keeps going for as long as a condition holds, the sentinel pattern that lets a loop read input until a special stop value shows up, and the small set of loop algorithms, summing, counting, and finding an extreme value, that appear inside almost every loop you will write this term.
Section 4.1
An if statement runs a block of code at most once. A while loop runs the same block over and over, checking the same condition again before every single pass.
A while loop repeats a block of indented statements for as long as its condition evaluates to True. The condition is tested before the body ever runs, and again before every repeat, which is why a while loop is called a pretest loop: if the condition is False the very first time, the body never executes at all, not even once.
while loop has the same three moving parts: something initialized before the diamond, the diamond's condition, and something inside the loop body that changes the value the diamond is testing. Leave any one of them out and the loop either never runs or never stops.The flowchart above is this exact program. Notice the loop variable count appears in all three places the diagram calls out: it is set once before the loop, tested in the while line, and changed once per pass through the body.
count = 1 # 1. initialize while count <= 5 : # 2. condition, tested before every pass print(count) count = count + 1 # 3. update, moves toward making the condition False print("Done")
Forget the update and the loop never ends. If the line count = count + 1 were deleted, count would stay 1 forever, count <= 5 would always be True, and the program would print 1 endlessly. This is an infinite loop, and it is the single most common mistake beginners make with while. Before running any loop you write, ask: "what in this body makes the condition eventually False?"
A loop whose number of passes is known in advance, like the one above, is called a definite loop. A loop that depends on something not known ahead of time, like user input, is an indefinite loop. Section 4.3 is built entirely out of indefinite loops.
"Forgot the update" is not the only way to write an infinite loop, and these three show up constantly in trace-the-code questions:
while i < 5 : paired with i = i - 1 moves i further from satisfying the condition every pass, not closer. Something changes every time, but the loop still never ends.!= can get skipped right over. while i != 9 : looks like it stops at 9, but if i starts at 1 and increases by 2, it visits 1, 3, 5, 7, 9 and actually would stop there. Change the step to i = i + 3 instead, and it visits 1, 4, 7, 10, 13…, sails straight past 9, and never stops. < or <= cannot be skipped over this way; != can.while line is not one of them:year = 0
rate = 5
principal = 10000
while year < 10 :
interest = (principal * year * rate) / 100
print("Interest", interest)
# year is never updated here -- infinite loop,
# even though interest changes every single pass
When tracing code, check the exact variable in the while line, not just whether something is changing. It is easy to see a line like interest = … changing every pass and assume the loop is making progress. Progress only counts if it is progress toward making that specific condition False.
A loop variable does not have to change only on the update line at the bottom. An if inside the body can reassign it too, which lets a loop skip ahead or cut itself short early:
i = 1
while i < 10 :
print(i, end = " ")
i = i + 2
if i == 5 :
i = 9
| Pass | i at print | i after i = i + 2 | if i == 5 fires? |
|---|---|---|---|
| 1 | 1 | 3 | no |
| 2 | 3 | 5 | yes → i = 9 |
| 3 | 9 | 11 | no — loop exits, 11 < 10 is False |
Output: 1 3 9. Notice 5 and 7 never print; the if jumped i from 5 straight to 9 before the next check. The lesson is the same as above: trace the condition's exact variable after every line that could touch it, including lines inside an if, not only the update line you expect.
Because the test happens before the body, the loop checks count <= 5 one extra time, at count == 6, and that is the check that finally stops it. Tracing every single check, not just the ones that pass, is the easiest way to catch an off-by-one mistake before it ever becomes a bug:
The loop variable does not have to be the thing you print. Here, number plays the same three-part role as count did above, while a second variable, total, builds up a running sum one pass at a time:
total = 0
number = 1
while number <= 10 :
total = total + number
number = number + 1
print("Sum:", total)
| Pass | number | total after this pass |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 3 | 6 |
| 4 | 4 | 10 |
| 5 | 5 | 15 |
| … | … | … |
| 10 | 10 | 55 |
This pattern, a variable that starts at 0 and gets added to once per pass, is called an accumulator. It comes back constantly in Section 4.5.
A string's characters can be visited the same way: an index variable plays the loop-variable role, and s[i] pulls out one character per pass. Forward, starting the index at 0 and stopping before len(s), visits every character; starting the index at len(s) - 1 and stopping once it drops below 0 visits them in reverse:
s = "abcde" i = 0 while i < len(s) : # forward print(s[i]) i = i + 1 # prints a b c d e j = len(s) - 1 while j >= 0 : # backward print(s[j]) j = j - 1 # prints e d c b a
Starting the index at 1 instead of 0 silently skips the first character. s[0] is always the first character of a string, never s[1]. A forward loop that starts its index at 1 prints every character except the first one, and nothing about it looks wrong at a glance, which is exactly why trace-the-code questions like to start the index one too high.
Section 4.3
The loops in Section 4.1 all knew their count in advance: five rides, ten numbers. A sentinel loop is for when you have no idea how many inputs are coming, and the user tells the program when to stop.
A sentinel value is a special input value that is not real data at all; it exists only to signal "there is nothing more to process." A sentinel has to be something that could never be a legitimate value in the actual data, such as -1 for a list of quiz scores that are always 0 or higher, or an empty string for a list of names.
The read before the loop is called a priming read, and it exists so the while condition has something to test on its very first check. Without it, the condition would have nothing to compare on pass zero. The second read, at the bottom of the loop body, replaces that value with the next one, which is exactly what the condition checks the next time around.
total = 0
count = 0
score = int(input("Enter a score (-1 to stop): ")) # priming read
while score != -1 :
total = total + score
count = count + 1
score = int(input("Enter a score (-1 to stop): ")) # read next
print("Total:", total)
print("Count:", count)
Forget the second read and the loop never stops. If the line inside the loop that reads the next score is deleted, score keeps the same value every pass, the condition never becomes False on its own, and the loop runs forever on whatever the first value was. The update in a sentinel loop is the read.
Any value that can never legitimately appear in the data works as a sentinel, including an empty string. Pressing Enter with no text produces "", which makes a natural stop signal for a loop that collects names:
name = input("Enter a name (press Enter to stop): ") # priming read
count = 0
while name != "" :
count = count + 1
print("Hello,", name)
name = input("Enter a name (press Enter to stop): ") # read next
print("Greeted", count, "people")
A negative number is not automatically a safe sentinel. -1 only works as a sentinel for quiz scores because a real score can never be negative. If the data itself can legitimately include negative numbers, such as temperatures or account balances, no number is safe to use as a sentinel; a non-numeric value, like a blank line or the letter Q, is the only reliable choice.
The stop condition can also be a general rule instead of one magic number. A loop collecting only positive scores can simply stop the moment a value fails to be positive, letting the condition that defines "real data" double as the sentinel check:
total = 0
count = 0
value = int(input("Enter a positive value (0 or less stops): "))
while value > 0 :
total = total + value
count = count + 1
value = int(input("Enter a positive value (0 or less stops): "))
Here anything that is not positive ends the loop, not just one specific number. The logic is identical to the -1 version above, just phrased as a range instead of an exact match.
Instead of testing the sentinel directly in the while line, some programs use a separate bool variable, often named done, that starts False and gets explicitly set to True once the stop condition is detected inside the loop. The loop shape is while not done : instead of while value != sentinel ::
done = False
while not done :
letter = input("Enter a letter (Q to stop): ")
if letter == "Q" :
done = True
else :
print("You entered:", letter)
A flag loop still needs the same three parts as any other while loop. done is initialized before the loop, tested in the condition, and set inside the body, exactly like count or a sentinel value were in earlier examples. The only difference is what changes the condition: a direct comparison to input versus a flag an if sets explicitly. Forgetting the done = True line produces the same infinite loop as forgetting any other update.
If there is any chance a sentinel might never show up, such as reading from a file that could be malformed, combining the sentinel check with a maximum count using and keeps the loop from running forever:
count = 0
value = input("Enter a value, Q to quit: ")
while count < 100 and value != "Q" :
count = count + 1
value = input("Enter a value, Q to quit: ")
This loop stops whichever comes first: the user typing Q, or 100 values having been entered. Both halves of the and have to still be true for the loop to continue.
Sentinel loops versus validation loops. Section 3.9's input validation loop keeps asking because the input it already received was bad, and stops the moment it becomes good. A sentinel loop is the opposite: every value it reads before the sentinel is already good data, and it stops only when the user deliberately signals there is no more of it. Same loop shape, two different jobs.
Section 4.5
Almost every loop you will write this term is a small variation on one of three patterns: adding things up, counting things, and finding the biggest or smallest thing. Learn the shape once and you will recognize it everywhere.
This is the accumulator from Section 4.1, applied to sentinel-terminated input from Section 4.3. total and count are both initialized to 0 before the loop; the average is only computed after the loop, and only if at least one value was actually entered.
total = 0
count = 0
score = int(input("Enter a score (-1 to stop): "))
while score != -1 :
total = total + score
count = count + 1
score = int(input("Enter a score (-1 to stop): "))
if count > 0 :
average = total / count
print("Average:", average)
else :
print("No scores were entered.")
Always guard the division. If the very first input is the sentinel, count stays 0 and total / count raises ZeroDivisionError. The if count > 0 check above exists for exactly that case.
A counting loop adds 1 to a counter only when a condition is met, instead of adding every value unconditionally. The accumulator and the condition from Section 4.3 combine directly:
passingCount = 0
score = int(input("Enter a score (-1 to stop): "))
while score != -1 :
if score >= 60 :
passingCount = passingCount + 1
score = int(input("Enter a score (-1 to stop): "))
print("Passing scores:", passingCount)
To track the largest value seen so far, use the first real value as a starting guess, then compare every later value against it, replacing the guess whenever something bigger comes along:
highest = int(input("Enter a score (-1 to stop): ")) # first value is the first guess
score = highest
while score != -1 :
if score > highest :
highest = score
score = int(input("Enter a score (-1 to stop): "))
print("Highest score:", highest)
Finding a minimum is the same algorithm with one symbol flipped. Change if score > highest : to if score < lowest :, and initialize lowest the same way, from the first real value, not from 0. Starting a minimum search at 0 is a common bug: it silently breaks the moment every real value happens to be above zero.
Some problems care about how a value relates to the one entered right before it, not the whole data set. The trick is a variable, usually named previous, that is set to the old value before the new one overwrites it:
letter = input("Enter a letter (Enter to stop): ")
while letter != "" :
previous = letter
letter = input("Enter a letter (Enter to stop): ")
if letter == previous :
print("Duplicate entry:", letter)
Order matters here. previous = letter has to happen before letter is overwritten by the next read, or there is nothing left to compare against. Reversing those two lines compares a value to itself, which is always true and defeats the whole point.
A search loop stops as soon as it finds what it is looking for, not after a fixed number of passes. The usual shape pairs a bool flag with a position counter, and the loop condition checks both: keep going while not yet found and still more to check.
s = "abcdEfghI"
position = 0
found = False
while position < len(s) and not found :
if s[position].isupper() :
found = True
else :
position = position + 1
print("First uppercase letter is at position", position)
Both halves of the condition are doing real work. not found stops the loop the instant a match turns up, so it does not keep scanning past it. position < len(s) stops the loop even if nothing ever matches, so it does not run off the end of the string. Drop either half and the loop breaks: without not found it keeps scanning past every match; without the length check it crashes on a string with no uppercase letter at all.
| Goal | Before the loop | Each pass through the loop | After the loop |
|---|---|---|---|
| Sum / average | total = 0, count = 0 | total += value, count += 1 | average = total / count |
| Counting matches | matchCount = 0 | if condition: matchCount += 1 | use matchCount directly |
| Maximum / minimum | best = first value | if value beats best: best = value | use best directly |
| Comparing consecutive values | read one value first | previous = current, read next, compare | — (acts as it goes) |
| Searching until found | found = False, position = 0 | if match: found = True, else advance position | use position or found |
Nothing stops a single loop from updating more than one accumulator at a time. This version computes the sum, the count, and the highest score, all from the same sentinel-terminated input, in one trip through the data:
total = 0
count = 0
score = int(input("Enter a score (-1 to stop): "))
highest = score
while score != -1 :
total = total + score
count = count + 1
if score > highest :
highest = score
score = int(input("Enter a score (-1 to stop): "))
if count > 0 :
print("Average:", total / count)
print("Highest:", highest)
Three bugs these patterns hide in plain sight. input() always returns a str, even when the prompt asks for a number. total = total + value without wrapping the read in int() or float() first raises a TypeError the moment arithmetic is attempted on text, and value > 0 does the same for a comparison. A line that uses a name that was never assigned, such as printing letter when the variable is actually called s[position], raises a NameError instead. And a missing closing parenthesis on a line like value = int(input("Enter a value: ") is a SyntaxError that stops the program before it ever runs a single line. When code "looks right" but the question is about what happens when it runs, check the type of every value coming from input() first.
while loop is a pretest loop: the condition is checked before the body runs, including the final check that stops it. Every while loop needs three parts: initialize, condition, and an update that eventually makes the condition False.!= paired with a step size that can skip right past the stopping value.if inside the body, not just by the update line at the bottom. Trace it after every line that could touch it.done flag variable set True inside the loop works the same way, just with the stop condition spelled out in an if instead of the while line.count > 0 before computing an average. Initialize a maximum, minimum, or "previous value" search from the first real value, never from 0.input() always returns a str. Forgetting int() or float() before doing arithmetic or a numeric comparison on it raises a TypeError, not a wrong answer, when the code actually runs.