Loops in Python are driven by four control keywords: break, continue, pass, and the loop's optional else clause. These keywords let you say, inside the body of a loop, "stop completely", "skip to the next iteration", "do nothing", or "run this only if the loop exhausted naturally". Knowing exactly what each one does makes loop code intentional rather than accidental.
break exits the innermost loop immediately — the iteration is abandoned, no later iterations happen, and the else clause (if any) is skipped. The classic use is a search: iterate until you find a matching item, then break because the rest of the list no longer matters. Do not use break to signal success to an outer loop; if you need that, set a flag or raise a narrow exception.
continue jumps to the next iteration without executing the rest of the body. It is the idiomatic way to filter out unwanted items while keeping the main path at a shallow indent. for line in f: if not line.strip(): continue; process(line) reads better than the equivalent if line.strip(): process(line) when process() is several lines long.
pass is a no-op placeholder. Use it when the Python grammar requires a statement but you have nothing to say yet: an empty except that silences a specific error, a stub method, or a class body you intend to fill in later. It is distinct from continue: pass does nothing and the loop moves on normally; continue actively skips to the next iteration.
The for/else and while/else clauses are Python's most misunderstood feature. The else block runs only if the loop finished without executing a break. Once you remember that it means "did the search fall through to the end?", it makes perfect sense: you break when you found the target, and the else branch is where you handle "not found". It is equivalent to a boolean flag but removes the possibility of getting the flag wrong.
The for/else pattern (found vs not found)
The typical shape is: iterate, break when the target appears, and use else to handle the "nothing matched" case. Compared to a found = False flag set inside the loop, the else form keeps control flow and data flow in the same place — you cannot forget to flip the flag.
Controlling nested loops
Python has no labelled break. When you need to exit two loops at once, extract the inner logic into a function and return from it, or raise a narrow exception caught just outside the outer loop. Both are clearer than boolean flags shared between loop bodies.
The handful of keywords and helpers that shape how a loop runs.
| Tool | Purpose |
|---|---|
breakstatement | Exits the innermost enclosing loop immediately. |
continuestatement | Skips the rest of the current iteration. |
passstatement | Do-nothing placeholder; loop continues normally. |
for ... elseclause | Runs only when the loop completes without break. |
while ... elseclause | Runs only when the condition becomes false without break. |
any() / all()built-ins | Replace many break-driven searches with one expression. |
itertools.takewhile()function | Yield items while a predicate holds; stops on first false. |
itertools.dropwhile()function | Skip items while a predicate holds; then yield the rest. |
Controlling Loop Execution code example
The example searches for the first prime in a list using break/else, filters a log with continue, and shows pass as a stub.
# Lesson: Controlling Loop Execution
# Goal: contrast break, continue, pass and the for/else clause.
def is_prime(n: int) -> bool:
'''Trial division with an early exit via `break`.'''
if n < 2:
return False
for d in range(2, int(n ** 0.5) + 1):
if n % d == 0:
return False # found a divisor -> not prime
return True
def first_prime(candidates: list[int]) -> int | None:
'''Find the first prime using for/else; returns None if none matched.'''
for n in candidates:
if is_prime(n):
return n
else:
return None # for/else runs because we never broke out
def clean_lines(lines: list[str]) -> list[str]:
'''Strip and drop blank or comment lines (continue keeps the path flat).'''
out = []
for raw in lines:
line = raw.strip()
if not line:
continue
if line.startswith("#"):
continue
out.append(line)
return out
def stub(x):
pass # placeholder; filled in a future lesson
# --- main script ---------------------------------------------------------
print("first prime:", first_prime([15, 21, 22, 29, 35]))
print("no primes: ", first_prime([4, 6, 8, 10]))
print("clean: ", clean_lines(["a", "", " # comment", "b"]))
stub(42) # returns None, as intended
Trace each control-flow choice:
1) `return False` inside is_prime() abandons the trial-division loop.
2) first_prime() uses for/else: the else runs only when no break occurred.
3) `continue` in clean_lines() keeps the accept path at indent level one.
4) `pass` lets stub() exist as a valid function body while empty.
Two practice snippets that show common mistakes and their fixes.
# Example A: replace a boolean flag with for/else
items = [2, 4, 6, 8]
target = 7
for it in items:
if it == target:
print("found")
break
else:
print("missing")
# Example B: escape a nested loop by extracting to a function
def find_pair(matrix, target):
for row in matrix:
for v in row:
if v == target:
return row, v
return None
print(find_pair([[1, 2], [3, 4]], 3))
Assertions that pin the semantics of each keyword.
assert first_prime([4, 6, 9, 11]) == 11
assert first_prime([4, 6, 9]) is None
assert clean_lines([" keep ", "", "#x", "#y", "also"]) == ["keep", "also"]
assert stub("anything") is None
Running the script prints:
first prime: 29
no primes: None
clean: ['a', 'b']