Skip to content

My loop never stops

  • Python · Foundations
  • 20 min read, about 1 h 10 min with practice
  • Concept
  • Asked often: medium
  • Reviewed 2026-11

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?

  • Input: a list of lines, as typed: numbers, maybe blank lines, maybe spaces, and maybe a stop line.
  • Output: the sum of the numbers before the first stop. Skip lines that are empty or only spaces. Treat Stop and stop like stop. With no stop, 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.

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".

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"]:

Turnilines[i]What runstotal
10"2"Add 2, then i += 12
21""continue: back to the top, i stays 12
31""continue again2
41""…and again, forever2

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.

All three look at each line at most once, so all take O(n) time for n lines, when they finish.

Okay

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.

Better

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 total

Where 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.

Best

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 total

When 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.

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

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: total_until_stop(['2', '3', 'stop', '100']) → 5
  2. Not run yet: total_until_stop(['4', '1']) → 5
  3. Not run yet: total_until_stop(['2', '', '3', 'stop']) → 5
  4. Not run yet: Extra test
  5. Not run yet: Extra test
  6. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
WayBlank lineSpaces or StopGoodNot so goodUse it when
Okay: while and iNever endsCrashes, if it gets thereYou see every stepOne forgotten i += 1 hangs itYou don’t know how many turns, like “until the user quits”
Better: forSkippedCrashesCan’t get stuckTrusts tidy typingData you made yourself
Best: for and strip()SkippedHandledCan’t get stuck, takes messy typingOne more lineAnything people type
  • The order of elifs: the first true test wins. Check % 15 before % 3, and >= 90 before >= 80.
  • = and ==: = gives a name a value; == compares. if x = 5: is a SyntaxError.
  • Off by one: range(1, 10) stops at 9. Use range(1, n + 1) to include n.
  • Changing a list while you loop over it skips items. The comprehensions lesson shows why.
  • for … else: the else runs only when the loop ended without break. It reads “no break”, not “otherwise”.
  • Stuck program: press Ctrl+C in the terminal to stop it.

Questions to ask first

  • What counts as a blank line? Only "", or spaces too?
  • Is stop case-sensitive?
  • What should happen to text that isn’t a number?

A 60-second walkthrough

“I loop with for over the lines, so the loop always advances, even after continue. For each line I strip whitespace and lowercase it. An empty result means a blank line, so I continue. stop means break. Otherwise I convert with int() and add. It’s one pass, O(n), and constant extra space. The bug in the while version was that continue skipped the i += 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 … else do? The else block runs if the loop finished without break, which is useful for “not found”.
  • How do you loop with the position too? for i, item in enumerate(items):.

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.

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

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: fizzbuzz(5) → ['1', '2', 'Fizz', '4', 'Buzz']
  2. Not run yet: fizzbuzz(15)
  3. 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
Okay

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.

Better

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 out
Best

Build 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 out

When Best isn’t best: with only two rules, Better is just as clear. Pick the one you can explain fastest.

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

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: first_hot_day([20, 31, 25, 33], 30) → 1
  2. Not run yet: first_hot_day([20, 21], 30) → -1
  3. Not run yet: Extra test
  4. Not run yet: Extra test
  5. 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
Okay

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.

Better

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 day

Where 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.

Best

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 -1

When Best isn’t best: if you ask this question many times about the same days, sort or index them once instead.

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

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: play(7, [5, 9, 7, 2]) → ['higher', 'lower', 'correct']
  2. Not run yet: play(7, [1, 2]) → ['higher', 'higher', 'out of tries: it was 7']
  3. Not run yet: play(7, [1, 2, 3, 4, 5, 6]) → ['higher', 'higher', 'higher', 'higher', 'higher', 'out of tries: it was 7']
  4. Not run yet: Extra test
  5. 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
Okay

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.

Better

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 replies

Where it breaks: six wrong guesses get six replies, and the player is never told they ran out.

Best

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 replies

When 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.

A few quick questions. Ones you miss are remembered, so you can find your weak spots later.

  1. Predict the outputWhat does this print?
    score = 95
    if score >= 80:
        print("B")
    elif score >= 90:
        print("A")
    else:
        print("C")
  2. Spot the bugThis loop never ends when items holds a negative number. Why?
    i = 0
    while i < len(items):
        if items[i] < 0:
            continue
        print(items[i])
        i += 1
  3. Predict the outputWhat does this print?
    for n in [1, 3, 5]:
        if n % 2 == 0:
            print("even")
            break
    else:
        print("none")
  4. Multiple choiceWhich values count as false in an if?

Add these to your review deck. Each card comes back a little later every time you get it right.

  • 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.
  • break leaves the loop; continue skips to the next turn; for … else runs the else when there was no break.
  • range(start, stop, step) stops before stop. enumerate(items) gives each position with its item.

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