My loop never stops
See the problem
Section titled “See the problem”You keep a running total of what people pay at a stall. Each payment is one line you type, and you type stop at the end. Your program reads the lines into a list and adds them up:
lines = ["2", "", "3", "stop"]You pressed Enter twice by mistake, so there’s an empty line. You run the program, and it never finishes. No error, no total. You press Ctrl+C to stop it.
What made the loop go round forever?
Define the task
Section titled “Define the task”- Input: a list of lines, as typed: numbers, maybe blank lines, maybe spaces, and maybe a
stopline. - Output: the sum of the numbers before the first
stop. Skip lines that are empty or only spaces. TreatStopandstoplikestop. With nostop, add every number. - The hard part: a loop must always move on, even on the lines it skips.
Example: total_until_stop(["2", "", " 3 ", "Stop", "100"]) gives 5.
Pause and think
Section titled “Pause and think”Hints open one at a time:
Hint 1 of 3
continue jumps straight back to the top of the loop. Any line after it in the loop body is skipped.
Hint 2 of 3
A for loop moves to the next item by itself, so continue can’t get it stuck.
Hint 3 of 3
line.strip() removes spaces at both ends, so " " becomes "", and " Stop " becomes "Stop".
Understand the idea
Section titled “Understand the idea”if, elif, else. Python checks each condition from the top and runs the first block that’s true. Later conditions aren’t checked. So put the most specific test first:
if score >= 90: grade = "A"elif score >= 80: # only checked when score < 90 grade = "B"else: grade = "C"A condition can be any value. Empty things count as false: 0, "", [] and None. Everything else counts as true. Join tests with and, or and not, or chain them: 18 <= age < 65.
for and while. for item in items: runs its block once for each item, and moves on by itself. for i in range(5): counts 0 to 4. while condition: repeats as long as the condition is true. It moves on only if your code changes something.
break and continue. break leaves the loop at once. continue skips the rest of this turn and goes back to the top.
A dry-run table follows a loop by hand, one row per turn. Here is the Okay rung below, on ["2", "", "3", "stop"]:
| Turn | i | lines[i] | What runs | total |
|---|---|---|---|---|
| 1 | 0 | "2" | Add 2, then i += 1 | 2 |
| 2 | 1 | "" | continue: back to the top, i stays 1 | 2 |
| 3 | 1 | "" | continue again | 2 |
| 4 | 1 | "" | …and again, forever | 2 |
Row 2 never reaches i += 1, so every turn after it is the same. In the editor below, press Visualize on a test to step through your own loop like this.
Climb the ladder
Section titled “Climb the ladder”All three look at each line at most once, so all take O(n) time for n lines, when they finish.
while with an index
The idea: Keep a position i, and add 1 to it at the end of each turn.
On ["2", "3", "stop"] it adds 2 and 3, sees stop and breaks. The total is 5.
def total_until_stop(lines: list[str]) -> int: total = 0 i = 0 while i < len(lines): if lines[i] == "": continue # skip a blank line if lines[i] == "stop": break total += int(lines[i]) i += 1 return total- Time
O(n)- Space
O(1)- Why
- One turn per line, if it ends.
Where it breaks: on a blank line, continue jumps back to the top before i += 1. i never changes, so the loop looks at the same blank line forever.
for, with continue and break
The idea: Loop with for line in lines, which moves on by itself.
The waste in Okay: it moves the position by hand, and one path forgets to.
On ["2", "", "3", "stop"]: the blank line hits continue, and for moves to "3". The total is 5.
Highlighted lines are new in this rung.
def total_until_stop(lines: list[str]) -> int: total = 0 for line in lines: if line == "": continue # skip a blank line if line == "stop": break total += int(line) return total- Time
O(n)- Space
O(1)- Why
- One turn per line, at most.
What changed from the previous rung
def total_until_stop(lines: list[str]) -> int: total = 0 i = 0 while i < len(lines): if lines[i] == "": for line in lines: if line == "": continue # skip a blank line if lines[i] == "stop": if line == "stop": break total += int(lines[i]) i += 1 total += int(line) return totalWhere it breaks: a line of spaces isn’t "", so int(" ") raises ValueError. And Stop isn’t stop, so it’s read as a number and crashes too.
Tidy each line first
The idea: Strip spaces and lower the case, then test the tidy text.
The waste in Better: it tests the line exactly as typed, when people type spaces and capitals.
not text is true for an empty string, so one test skips both "" and " ".
Highlighted lines are new in this rung.
def total_until_stop(lines: list[str]) -> int: total = 0 for line in lines: text = line.strip().lower() if not text: continue # skip a blank line, even one of spaces if text == "stop": break total += int(text) return total- Time
O(n)- Space
O(1)- Why
- One turn per line, at most, plus one strip each.
What changed from the previous rung
def total_until_stop(lines: list[str]) -> int: total = 0 for line in lines: if line == "": continue # skip a blank line if line == "stop": text = line.strip().lower() if not text: continue # skip a blank line, even one of spaces if text == "stop": break total += int(line) total += int(text) return totalWhen Best isn’t best: a word like "two" still raises ValueError. When the lines come from people, catch it and say which line was wrong. The exceptions lesson shows how.
Write it yourself
Section titled “Write it yourself”Now write it in the page. Press Run tests to check your code. Python runs in your browser, so nothing is sent anywhere.
Try Okay from the ladder: the blank-line test runs out of time, and the tests after it don’t run. Press Visualize on a test to watch your loop step by step.
Add until stop
Ctrl + Enter runs- Not run yet:
total_until_stop(['2', '3', 'stop', '100'])→5 - Not run yet:
total_until_stop(['4', '1'])→5 - Not run yet:
total_until_stop(['2', '', '3', 'stop'])→5 - Not run yet: Extra test
- Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Compare
Section titled “Compare”| Way | Blank line | Spaces or Stop | Good | Not so good | Use it when |
|---|---|---|---|---|---|
Okay: while and i | Never ends | Crashes, if it gets there | You see every step | One forgotten i += 1 hangs it | You don’t know how many turns, like “until the user quits” |
Better: for | Skipped | Crashes | Can’t get stuck | Trusts tidy typing | Data you made yourself |
Best: for and strip() | Skipped | Handled | Can’t get stuck, takes messy typing | One more line | Anything people type |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- The order of
elifs: the first true test wins. Check% 15before% 3, and>= 90before>= 80. =and==:=gives a name a value;==compares.if x = 5:is aSyntaxError.- Off by one:
range(1, 10)stops at 9. Userange(1, n + 1)to includen. - Changing a list while you loop over it skips items. The comprehensions lesson shows why.
for … else: theelseruns only when the loop ended withoutbreak. It reads “no break”, not “otherwise”.- Stuck program: press Ctrl+C in the terminal to stop it.
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- What counts as a blank line? Only
"", or spaces too? - Is
stopcase-sensitive? - What should happen to text that isn’t a number?
A 60-second walkthrough
“I loop with
forover the lines, so the loop always advances, even aftercontinue. For each line I strip whitespace and lowercase it. An empty result means a blank line, so Icontinue.stopmeansbreak. Otherwise I convert withint()and add. It’s one pass, O(n), and constant extra space. The bug in thewhileversion was thatcontinueskipped thei += 1, so the index never moved.”
Likely follow-ups
- When would you use
while? When you don’t know the number of turns in advance, like reading until the user quits. - What does
for … elsedo? Theelseblock runs if the loop finished withoutbreak, which is useful for “not found”. - How do you loop with the position too?
for i, item in enumerate(items):.
Practice
Section titled “Practice”Three problems, from easy to hard. Each has hints, tests you can run here, and its own Okay, Better and Best answers. Try before you open them.
1. FizzBuzz (easy)
Section titled “1. FizzBuzz (easy)”Write fizzbuzz(n). Return a list of text for the numbers 1 to n. Use "Fizz" for numbers divisible by 3, "Buzz" for 5, and "FizzBuzz" for both. Any other number is written as itself, like "7".
Hint 1 of 3
x % 3 == 0 means x divides by 3 with nothing left over.
Hint 2 of 3
Which test must come first, so that 15 isn’t caught by the test for 3?
Hint 3 of 3
You can also build the word: add "Fizz" if it divides by 3, add "Buzz" if by 5.
FizzBuzz
Ctrl + Enter runs- Not run yet:
fizzbuzz(5)→['1', '2', 'Fizz', '4', 'Buzz'] - Not run yet:
fizzbuzz(15) - Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Test 3, then 5, then 15
The idea: An if for 3, an elif for 5, an elif for 15.
1 to 14 come out right.
def fizzbuzz(n: int) -> list[str]: out = [] for i in range(1, n + 1): if i % 3 == 0: out.append("Fizz") elif i % 5 == 0: out.append("Buzz") elif i % 15 == 0: out.append("FizzBuzz") else: out.append(str(i)) return out- Time
O(n)- Space
O(n)- Why
- One check per number, and a list of n answers.
Where it breaks: 15 divides by 3, so the first branch says Fizz, and the test for 15 is never reached. FizzBuzz never appears.
Both first
The idea: Test divisible by 3 and 5 first, then each on its own.
The waste in Okay: the most specific test sat last, where it can never win.
This passes every test. It is the answer most interviewers expect.
Highlighted lines are new in this rung.
def fizzbuzz(n: int) -> list[str]: out = [] for i in range(1, n + 1): if i % 3 == 0 and i % 5 == 0: out.append("FizzBuzz") elif i % 3 == 0: out.append("Fizz") elif i % 5 == 0: out.append("Buzz") else: out.append(str(i)) return out- Time
O(n)- Space
O(n)- Why
- The same work, in the right order.
What changed from the previous rung
def fizzbuzz(n: int) -> list[str]: out = [] for i in range(1, n + 1): if i % 3 == 0: if i % 3 == 0 and i % 5 == 0: out.append("FizzBuzz") elif i % 3 == 0: out.append("Fizz") elif i % 5 == 0: out.append("Buzz") elif i % 15 == 0: out.append("FizzBuzz") else: out.append(str(i)) return outBuild the word
The idea: Start with an empty word, add Fizz for 3 and Buzz for 5, and use the number if the word is still empty.
The waste in Better: it tests 3 and 5 twice each, and a new rule, like “Bazz” for 7, means rewriting every branch.
Here a new rule is one more if. word or str(i) gives the word, or the number when the word is empty.
Highlighted lines are new in this rung.
def fizzbuzz(n: int) -> list[str]: out = [] for i in range(1, n + 1): word = "" if i % 3 == 0: word += "Fizz" if i % 5 == 0: word += "Buzz" out.append(word or str(i)) return out- Time
O(n)- Space
O(n)- Why
- Two checks per number.
What changed from the previous rung
def fizzbuzz(n: int) -> list[str]: out = [] for i in range(1, n + 1): if i % 3 == 0 and i % 5 == 0: out.append("FizzBuzz") elif i % 3 == 0: out.append("Fizz") elif i % 5 == 0: out.append("Buzz") else: out.append(str(i)) word = "" if i % 3 == 0: word += "Fizz" if i % 5 == 0: word += "Buzz" out.append(word or str(i)) return outWhen Best isn’t best: with only two rules, Better is just as clear. Pick the one you can explain fastest.
2. The first hot day (medium)
Section titled “2. The first hot day (medium)”Write first_hot_day(temps, limit). Return the position of the first day hotter than limit, or -1 if there is none. The list can be very long, so stop as soon as you know.
Example: first_hot_day([20, 31, 25, 33], 30) gives 1.
Hint 1 of 3
Keeping the "day" each time you see a hot one gives you the last hot day, not the first.
Hint 2 of 3
Once you have found one, there is nothing left to learn from the rest.
Hint 3 of 3
return inside a loop leaves the loop and the function at once.
The first hot day
Ctrl + Enter runs- Not run yet:
first_hot_day([20, 31, 25, 33], 30)→1 - Not run yet:
first_hot_day([20, 21], 30)→-1 - Not run yet: Extra test
- Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Remember each hot day
The idea: Walk the days and set day = i whenever it’s hot.
[20, 31, 25] gives 1, which is right.
def first_hot_day(temps: list[float], limit: float) -> int: day = -1 for i in range(len(temps)): if temps[i] > limit: day = i return day- Time
O(n)- Space
O(1)- Why
- One look at every day.
Where it breaks: each hot day overwrites the last, so [20, 31, 25, 33] gives 3: the last hot day, not the first.
Only set it once
The idea: Set day only while it is still -1.
The waste in Okay: later hot days overwrite the answer.
Highlighted lines are new in this rung.
def first_hot_day(temps: list[float], limit: float) -> int: day = -1 for i in range(len(temps)): if temps[i] > limit and day == -1: day = i return day- Time
O(n)- Space
O(1)- Why
- Still one look at every day.
What changed from the previous rung
def first_hot_day(temps: list[float], limit: float) -> int: day = -1 for i in range(len(temps)): if temps[i] > limit: if temps[i] > limit and day == -1: day = i return dayWhere it breaks: it keeps looking after it has the answer. With a hot first day and a billion days after it, it checks every one of them.
Return at the first one
The idea: Return the day as soon as it is hot; return -1 after the loop.
The waste in Better: it looks at every day after the answer is known.
enumerate(temps) gives each position with its value, so there is no temps[i].
Highlighted lines are new in this rung.
def first_hot_day(temps: list[float], limit: float) -> int: for day, temp in enumerate(temps): if temp > limit: return day return -1- Time
O(n)- Space
O(1)- Why
- In the worst case it still looks at every day, but it stops at the first hot one.
What changed from the previous rung
def first_hot_day(temps: list[float], limit: float) -> int: day = -1 for i in range(len(temps)): if temps[i] > limit and day == -1: day = i return day for day, temp in enumerate(temps): if temp > limit: return day return -1When Best isn’t best: if you ask this question many times about the same days, sort or index them once instead.
3. Guess the number (hard)
Section titled “3. Guess the number (hard)”Write play(secret, guesses, tries=5). Go through the guesses in order and reply to each: "higher", "lower" or "correct". Stop after a correct guess. The player has only tries guesses. If they run out, or the guesses run out, add "out of tries: it was 7", with the secret.
Example: play(7, [5, 9, 7, 2]) gives ["higher", "lower", "correct"].
Hint 1 of 3
Stop the loop after "correct" with break.
Hint 2 of 3
guesses[:tries] keeps only the first tries guesses.
Hint 3 of 3
A for loop’s else block runs only if the loop ended without break.
Guess the number
Ctrl + Enter runs- Not run yet:
play(7, [5, 9, 7, 2])→['higher', 'lower', 'correct'] - Not run yet:
play(7, [1, 2])→['higher', 'higher', 'out of tries: it was 7'] - Not run yet:
play(7, [1, 2, 3, 4, 5, 6])→['higher', 'higher', 'higher', 'higher', 'higher', 'out of tries: it was 7'] - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Reply to every guess
The idea: An if, elif and else for each guess.
The replies are right, one by one.
def play(secret: int, guesses: list[int]) -> list[str]: replies = [] for guess in guesses: if guess < secret: replies.append("higher") elif guess > secret: replies.append("lower") else: replies.append("correct") return replies- Time
O(g)- Space
O(g)- Why
- One reply for each of the g guesses.
Where it breaks: the game goes on after correct, and there is no limit on tries.
break after correct
The idea: Leave the loop when the guess is right.
The waste in Okay: it keeps replying after the game is won.
Highlighted lines are new in this rung.
def play(secret: int, guesses: list[int]) -> list[str]: replies = [] for guess in guesses: if guess < secret: replies.append("higher") elif guess > secret: replies.append("lower") else: replies.append("correct") break return replies- Time
O(g)- Space
O(g)- Why
- At most one reply per guess.
What changed from the previous rung
def play(secret: int, guesses: list[int]) -> list[str]: replies = [] for guess in guesses: if guess < secret: replies.append("higher") elif guess > secret: replies.append("lower") else: replies.append("correct") break return repliesWhere it breaks: six wrong guesses get six replies, and the player is never told they ran out.
A limit, and for … else
The idea: Loop over guesses[:tries]; the else block adds the out-of-tries line when there was no break.
The waste in Better: it has no end except the end of the list.
The else belongs to the for, not the if. It runs when the loop used up its guesses without a break.
Highlighted lines are new in this rung.
def play(secret: int, guesses: list[int], tries: int = 5) -> list[str]: replies = [] for guess in guesses[:tries]: if guess < secret: replies.append("higher") elif guess > secret: replies.append("lower") else: replies.append("correct") break else: replies.append(f"out of tries: it was {secret}") return replies- Time
O(t)- Space
O(t)- Why
- At most t = tries guesses are looked at.
What changed from the previous rung
def play(secret: int, guesses: list[int]) -> list[str]:def play(secret: int, guesses: list[int], tries: int = 5) -> list[str]: replies = [] for guess in guesses: for guess in guesses[:tries]: if guess < secret: replies.append("higher") elif guess > secret: replies.append("lower") else: replies.append("correct") break else: replies.append(f"out of tries: it was {secret}") return repliesWhen Best isn’t best: many readers misread for … else. A flag like won = True before break, then if not won:, says the same thing in a way everyone reads.
Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”if/elif/else: the first true branch runs, so put the most specific test first.- Empty things are false:
0,"",[],{},None. for x in items:moves on by itself.while cond:moves on only if your code changes something.breakleaves the loop;continueskips to the next turn;for … elseruns theelsewhen there was nobreak.range(start, stop, step)stops beforestop.enumerate(items)gives each position with its item.
Go deeper
Section titled “Go deeper”- More control flow tools, in the Python tutorial:
if,for,range(),break,continueandelseon loops. - Truth value testing: which values count as false.
- Next: Your first functions: def and return.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started