While Loops and Flow Control
James finishes a for loop that checks every note in a list. "Works fine," he says, "but I only needed the first published one. It kept going through all 200 notes after it already found the answer."
Emma nods. "A for loop visits every item. That is its job. When you need a loop that stops the moment a condition changes, you want a while loop."
A while loop repeats a block of code as long as a condition stays True. Unlike a for loop (which walks through a collection), a while loop can run an unpredictable number of times. It might run zero times, ten times, or forever if the condition never changes. That last possibility is the main danger, and this lesson teaches you how to avoid it.
Your First while Loop
A while loop checks its condition before each iteration. When the condition is False, Python exits the loop:
counter: int = 0
while counter < 3:
print(counter)
counter += 1
print("Done")
Output:
0
1
2
Done
The loop runs three times. When counter reaches 3, the condition 3 < 3 is False and Python exits the loop.
The Infinite Loop Trap
James writes his first while loop and forgets counter += 1. The terminal fills with 0 repeating endlessly. Emma tells him to press Ctrl+C.
# BUG: This loop never ends!
counter: int = 0
while counter < 3:
print(counter)
# Forgot: counter += 1
"Every while loop needs a way to stop," Emma says. "If nothing inside the loop changes the condition, it runs forever. Before you write a while loop, always ask: what changes on each pass to bring this closer to ending?"
break: Exit a Loop Early
Sometimes you want to stop a loop before the condition becomes False. The break statement exits the loop immediately:
notes: list[str] = ["draft", "draft", "published", "draft"]
found_index: int = -1
index: int = 0
while index < len(notes):
if notes[index] == "published":
found_index = index
break # Stop searching, we found it
index += 1
print(found_index)
Output:
2
Without break, the loop would check the remaining notes unnecessarily. This is the pattern James wanted.
continue: Skip to the Next Iteration
The continue statement skips the rest of the current iteration and jumps back to the condition check:
counter: int = 0
processed: list[str] = []
while counter < 5:
counter += 1
if counter == 3:
continue # Skip processing for 3
processed.append(f"item-{counter}")
print(processed)
Output:
['item-1', 'item-2', 'item-4', 'item-5']
When counter is 3, continue skips append and jumps to the condition check. Notice counter += 1 comes before continue. If it came after, the loop would skip the increment and repeat on 3 forever.
pass: The Official "Do Nothing" Statement
You have been using ... (Ellipsis) as a placeholder for empty function stubs since Chapter 49. pass is Python's official "do nothing" statement for empty blocks inside if, for, and while:
def process_note(text: str) -> None:
"""Process a note. Implementation coming later."""
pass # Placeholder, does nothing yet
Both ... and pass work as placeholders, but pass is the conventional choice inside control flow blocks.
Sentinel Values with Flag Variables
A sentinel value signals a loop to stop. Instead of counting iterations, you use a boolean flag variable that flips when a condition is met:
notes: list[str] = ["draft", "review", "published", "draft"]
found_published: bool = False
position: int = 0
while not found_published and position < len(notes):
if notes[position] == "published":
found_published = True
else:
position += 1
if found_published:
print(f"First published note at index {position}")
else:
print("No published notes found")
Output:
First published note at index 2
The flag starts False and flips to True when the target is found. This pattern is useful when the stopping condition depends on data you discover inside the loop, not on a simple counter.
Testing While Loops
Test the outcomes, not the iteration count:
def find_first_published(notes: list[str]) -> int:
"""Return the index of the first 'published' note, or -1 if none."""
found: bool = False
position: int = 0
while not found and position < len(notes):
if notes[position] == "published":
found = True
else:
position += 1
if found:
return position
return -1
def test_finds_published() -> None:
assert find_first_published(["draft", "published", "draft"]) == 1
def test_returns_negative_one_when_missing() -> None:
assert find_first_published(["draft", "draft"]) == -1
def test_empty_list() -> None:
assert find_first_published([]) == -1
def test_first_item_published() -> None:
assert find_first_published(["published", "draft"]) == 0
Four tests cover the key scenarios: found in the middle, not found, empty list, and found at the start. Run uv run pytest to verify.
PRIMM-AI+ Practice: While Loop Predictions
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Part A: Will these while loops terminate? Write "terminates" or "infinite" for each, with a confidence score from 1 to 5.
# Loop 1
x: int = 10
while x > 0:
x -= 3
# Loop 2
y: int = 1
while y != 10:
y += 2
# Loop 3
z: int = 5
while z < 100:
z *= 2
Check your predictions
Loop 1: Terminates. x goes 10, 7, 4, 1, -2. At -2, x > 0 is False.
Loop 2: Infinite. y goes 1, 3, 5, 7, 9, 11, 13... It skips 10 entirely, so y != 10 is never False.
Loop 3: Terminates. z goes 5, 10, 20, 40, 80, 160. At 160, z < 100 is False.
Part B: Predict break vs continue. For each snippet, predict the output.
# Snippet 1
result: list[int] = []
i: int = 0
while i < 5:
i += 1
if i == 3:
break
result.append(i)
print(result)
# Snippet 2
result2: list[int] = []
j: int = 0
while j < 5:
j += 1
if j == 3:
continue
result2.append(j)
print(result2)
Check your predictions
Snippet 1: [1, 2]. When i becomes 3, break exits the loop before appending.
Snippet 2: [1, 2, 4, 5]. When j is 3, continue skips append but the loop keeps going.
Run
Press Shift+Tab to exit Plan Mode.
Create while_practice.py with the snippets above (add a safety break after 20 iterations to the infinite loop). Run uv run python while_practice.py and compare against your predictions.
Investigate + Modify
In Loop 2, change y != 10 to y < 10. Does it terminate now? Trace the values. Then add a break that exits when y > 10 and predict the final value of y.
If you want to go deeper, run /investigate @while_practice.py in Claude Code and ask why Loop 2 is infinite with != 10 but terminates with < 10.
Exercises
Exercise 1: Spot the Bug
This function has an infinite loop bug. Find it and fix it:
def count_drafts_before_published(notes: list[str]) -> int:
"""Count draft notes before the first published note."""
count: int = 0
index: int = 0
while index < len(notes):
if notes[index] == "published":
break
if notes[index] == "draft":
count += 1
# Bug is here: what's missing?
return count
Hint
The loop checks index < len(notes) but never changes index. What line needs adding?
Solution
Add index += 1 at the end of the loop body, inside the while:
while index < len(notes):
if notes[index] == "published":
break
if notes[index] == "draft":
count += 1
index += 1 # This was missing
Exercise 2: Write the Tests
Write collect_until_stop(words: list[str]) -> list[str] that collects words using a while loop with a flag variable. Stop when "STOP" is encountered (do not include it). If no "STOP", return all words. Write four tests: "STOP" in the middle, "STOP" first, no "STOP", and empty list.
Starter code
def collect_until_stop(words: list[str]) -> list[str]:
"""Collect words until 'STOP' is encountered."""
result: list[str] = []
hit_stop: bool = False
index: int = 0
# Your while loop here using hit_stop as sentinel
...
return result
def test_stop_in_middle() -> None:
assert collect_until_stop(["go", "run", "STOP", "skip"]) == ["go", "run"]
# Write test_stop_first, test_no_stop, test_empty
Make [Mastery Gate]
Without looking at any examples, write a function called search_notes(notes: list[str], target: str) -> int that uses a while loop with a flag variable to find the first occurrence of target in notes. Return the index, or -1 if not found. Do not use break.
Then write four test functions covering: target found, target missing, empty list, and target at the last position.
Run uv run pytest to verify all tests pass.
In SmartNotes, a while loop with a sentinel flag is how you might scan notes for the first one matching a filter: "keep checking until I find it or run out of notes." The for loop from Lesson 2 processes every note. The while loop from this lesson finds one specific note and stops.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Check Your Understanding
I'm learning about while loops in Python. Here is my
understanding: "A while loop and a for loop do the same
thing. You can always use either one."
Is my summary accurate? What am I missing?
Did the AI explain when while is a better fit than for? Compare its answer to what Emma told James.
Prompt 2: Generate and Review
Write a Python function that uses a while loop with a flag
variable to find the first negative number in a list of
integers. Return its index, or -1 if all numbers are
positive. Use type annotations on all variables and the
return type. Include a docstring.
Review the output. Does the flag variable control the loop? Is the index always incremented (no infinite loop risk)? Are all variables type-annotated?
What you're learning: You are applying the sentinel pattern and infinite-loop checklist from this lesson to evaluate AI-generated code.
Prompt 3: Compare for vs while
When should I use a for loop vs a while loop in Python? Give me
two short examples: one problem where a for loop is the right
choice and one where a while loop is better. Explain why each
loop type fits its problem.
Read the AI's response and compare it to what you learned in this lesson. The key distinction: for is for "visit every item," while is for "stop when a condition changes." Check whether the AI's examples match that rule. If both examples could work with either loop type, ask the AI for a stronger example where only while makes sense.
What you're learning: You are building judgment about when to reach for each loop type, which prevents the common mistake of forcing a for loop into a situation that needs early termination.
Python supports an else clause on loops. The else block runs only if the loop finishes without hitting break. Example: for n in [2,4,6]: / if n % 2 != 0: break / else: print("All even"). This is a niche idiom that many Python developers avoid. A flag variable (as taught above) is usually clearer. If you encounter loop/else in someone else's code, now you know what it means.
"Four things," James says, holding up fingers. "One: while repeats until the condition goes False. Two: break exits immediately when you find what you need. Three: continue skips one pass but keeps the loop going. Four: every while loop needs a way to stop, or you get an infinite loop." He drops his hand. "That last one is like a purchase order approval chain. If nobody in the chain has authority to sign off, the request just circulates forever. You need someone who can say 'approved' and end the cycle."
"You forgot pass," Emma says.
"Placeholder for empty blocks. Same idea as ... but more conventional inside control flow. Five things, then."
Emma nods. "And the sentinel flag -- the boolean that flips when you find your target. That's six." She pauses. "Honestly, I'm not sure I would have counted the flag separately. It's more of a pattern than a keyword. But it's the cleanest way to test whether a while loop found what it was looking for."
"So I've got for for 'visit everything' and while for 'stop when something changes.' What happens when I need a loop inside a loop?"
"Lesson 5. Nested loops. The inner one runs completely for every single step of the outer one. Multiply, don't add."