for/else
Python lets a loop carry an else clause, and the clause runs only when the loop ends without
a break. That makes it the language's built-in answer to a very common question: "I scanned
for something; did I find it?" Without it you'd keep a found flag or a sentinel and test it
after the loop. Two solutions use the clause: the sink search inside the topological sort of
problem 79, which is the textbook case, and the pentagonal pair search of problem 44, which uses
it on a while loop as a marker for "this can't happen". This page is part of the
language-idioms series. It covers the idiom, why these solutions use it, and how the same thing
looks in C, which has no else on its loops and uses a flag, a sentinel or a goto instead.
The idea
The Python tutorial
states the rule in one line: a loop's else runs when the loop finishes normally. For for, that
means the iterable ran out. For while, it means the condition became false. Anything that
leaves the loop early skips it: break, but also return and an exception. continue has no
effect on it. An empty iterable counts as finishing normally, so the else runs straight away.
The name is the stumbling block. else doesn't pair with the loop's body the way it pairs with an
if's. The better reading is "no break". In a search loop the break means "found", so the
else means "not found":
for item in items:
if matches(item):
break # found: skip the else
else:
handle_missing() # ran off the end: nothing matched
use(item) # safe: we only get here after a break
The last line matters. A Python loop variable outlives the loop, so after a break it still names
the item that matched. If the else raises or returns, every path that reaches the code after
the loop has a valid item. The loop variable is the result, and the else guarantees it's
set.
Passcode ordering (79). The solution builds a successor set for each digit and then takes the digits off from the right: on each round it finds a sink (a digit with no remaining successors), prepends it to the passcode, and removes it from the graph. This is topological sorting run sink-first. The search for a sink is exactly the shape above:
for char, after in successor_graph.items():
if not after:
passcode = char + passcode
break
else:
raise ValueError("No successor found - possible circular dependency in the constraints")
del successor_graph[char]
The else is the cycle detector. If every remaining digit still has a successor, the constraints
contain a cycle and no passcode exists, so the loop runs off the end and the solution raises.
Otherwise char is the sink, and the line after the loop removes it. There's no flag and no
"not found" value to check: the two outcomes are two branches of the same statement.
Pentagonal pairs (44). Here the clause sits on a while loop, and it does a different job:
i = 0
while i := (i + 1):
for j in range(i - 1, 0, -1):
if ...:
return str(p_i - p_j)
else:
raise ValueError("No solution found")
This else can never run. The walrus condition i := i + 1
yields 1, 2, 3, …, which are all truthy, so the condition never becomes false, and the only way
out is the return, which skips the else. The clause works as a comment the interpreter
enforces: "if this loop ever stops on its own, something is wrong". That's a fair intent, but a
plain while True: (or for i in itertools.count(1):) followed by a raise after the loop
says the same thing without making the reader work out when the walrus becomes falsy. Note too
that 44 needs to leave two nested loops at once. break would only leave the inner one, and
it's the return that gets out of both. Putting a search in its own function and returning from
it is the cleanest way out of nested loops in either language.
The counterpart in C
C loops have no else, so the "did the loop break?" fact has to be stored somewhere or jumped
over. The solutions show the first of three standard forms:
-
A sentinel result (79). C's sink search starts with
int found = -1;, setsfound = ibefore itbreaks, and checksif (found < 0)after the loop to report the cycle. One variable holds both the flag and the result. This works because −1 can never be a valid index; see sentinel value. It's the form most C code uses. -
The loop index itself. If the index is declared outside the loop, the loop's own exit test doubles as the flag:
int i; for (i = 0; i < n; i++) if (matches(i)) break; if (i == n) { /* the else: nothing matched */ }This is the closest match to
for/else. The "ran off the end" condition is the loop condition, andisurvives the loop just as a Python loop variable does. It only works when the loop counts up to a known bound. -
goto.for (…) if (matches(i)) goto found;, then the not-found code, then afound:label. This mirrors the Python structure exactly, and unlikebreakit also leaves several nested loops in one jump. Leaving nested loops is one of the few uses ofgotothat C style guides generally accept. The cost is a label that a reader has to look for.
Problem 44's C version shows the other half. for (long long i = 1; ; i++) has no condition, so
like the Python while it can only exit by return. The return -1 after it is the C version of
the unreachable else: dead code kept so the function doesn't fall off its end without a return
value.
Going from C to Python, the trap is copying the flag: found = False, set it in the loop, test
it afterwards. That works, but it's three extra lines and one more variable that can get out of
sync. For a pure lookup, next((c for c, a in graph.items() if not a), None) collapses the
search to one expression with a None sentinel, and any(...) answers a yes/no question with no
loop at all. for/else is the right choice when the found branch has to do something with
the item and the not-found branch has to raise or return.
How to reason about it
- Read
elseas "no break". It belongs to thebreak, not to the loop body. If the loop has nobreak, theelsealways runs (unless something returns or raises), and it's probably not what you meant. - Use it when "not found" is an error. The strongest form is the one in 79:
breakon the hit,raisein theelse, and use the loop variable after the loop, knowing it's set. - Don't use it to mark unreachable code. An
elseon a loop that can't finish normally (44) only tells the reader something if they first prove that it can't run.while True:plus a trailingraisesays it directly. - Nested loops need a function.
breakandelseonly apply to the innermost loop. To stop a nested search, put it in a function andreturn(both languages), or usegotoin C. - In C, choose by what you need afterwards. Need the index → test it against the bound.
Need a separate result → a sentinel such as −1. Need to leave nested loops →
gotoor an earlyreturn. - Prefer
next/anyfor pure lookups. When there's nothing to do on a hit but get the value, a generator expression is shorter than any loop.