else on Loops

Python's for and while loops can carry an else clause — one of the language's most misunderstood features — which runs only when the loop finishes normally, without being interrupted by break.

An else clause attached to a for or while loop runs exactly once, after the loop finishes — but only if the loop was never exited via break. This is unrelated to how else works on an if statement, and the naming is widely considered one of Python's more confusing design choices; understanding the precise rule removes the confusion entirely.

The Exact Rule

A loop's else block runs if and only if the loop completed without executing a break statement. This includes the case where the loop body never ran at all (an empty iterable, or a while condition that was false from the start) — that still counts as 'completed without break', so else runs.

A better mental model than 'else': think of it as 'no-break'. for ... else: reads more intuitively as 'for ... no-break:' — the block that runs when the search *didn't* find a reason to break early.

for i in range(3):
    print(i)
else:
    print('loop finished normally')
# 0 1 2
# loop finished normally

for i in range(3):
    if i == 1:
        break
    print(i)
else:
    print('this will not print')
# 0  -- else is skipped because 'break' fired

for...else

for...else is most useful in search patterns: loop looking for something, break as soon as it's found, and let else handle the 'not found' case cleanly, without a separate flag variable.

def find_first_even(numbers):
    for n in numbers:
        if n % 2 == 0:
            print(f'Found even number: {n}')
            break
    else:
        print('No even numbers found')

find_first_even([1, 3, 5, 8, 9])   # Found even number: 8
find_first_even([1, 3, 5, 9])      # No even numbers found

while...else

while...else follows the identical rule: else runs if the while condition eventually became falsy naturally, and is skipped if a break fired first. It's less common than for...else but appears in the same kind of 'search until condition or exhaustion' pattern.

In this example the else is actually redundant with a bare return True placed after the loop at the same indentation — because there's no break involved (a return inside the loop exits the function directly, bypassing else entirely, same as break would bypass it for a plain loop). It's included here to show the syntax; in real code, prefer whichever form is clearer.

def is_prime(n):
    if n < 2:
        return False
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False   # not prime
        i += 1
    else:
        return True         # loop completed without finding a divisor

print(is_prime(17))   # True
print(is_prime(18))   # False

Normal Completion vs. break: Side-by-Side

The clearest way to internalize the rule is to see both outcomes of the same loop shape next to each other.

continue and exceptions behave differently from break here: continue never prevents else (it only affects one iteration, the loop still eventually completes or breaks on its own terms), while an *uncaught* exception propagates out of the loop entirely, skipping both the rest of the loop and its else clause — the loop never 'completed' in either sense.

# Case 1: loop completes normally (iterable exhausted) -- else RUNS
for x in [1, 2, 3]:
    print(x)
else:
    print('else ran')

# Case 2: loop exits via break -- else is SKIPPED
for x in [1, 2, 3]:
    if x == 2:
        break
    print(x)
else:
    print('else ran')   # not printed

Practical Example: Membership Check Without a Flag Variable

Before for...else, a search-and-report pattern typically needed a separate boolean flag to track whether the item was found. for...else removes that flag entirely, keeping the found/not-found logic co-located with the search itself.

# Without for...else -- needs an explicit flag
def contains_negative_v1(numbers):
    found = False
    for n in numbers:
        if n < 0:
            found = True
            break
    if not found:
        print('No negative numbers')

# With for...else -- no flag needed
def contains_negative_v2(numbers):
    for n in numbers:
        if n < 0:
            break
    else:
        print('No negative numbers')

The Common Misunderstanding

The most frequent misreading is assuming else on a loop behaves like else on an if — i.e. 'run this if the loop's condition was false' or 'run this if the loop did nothing'. Neither is correct. It has nothing to do with the loop's condition being true or false at any particular check; it is entirely about whether break executed. A loop that runs zero times still triggers else (no break occurred, trivially), which surprises people expecting 'else' to mean 'the loop had no iterations'.

for x in []:            # loop body never runs -- empty iterable
    print(x)
else:
    print('runs anyway')   # prints, because no break occurred (there was nothing to break from)

When to Use It (and When Not To)

for...else/while...else earns its keep specifically in search-until-found-or-exhausted patterns where you'd otherwise need a flag variable. Outside that narrow case, many Python developers (and style guides) consider it confusing enough that an explicit flag, or restructuring the search into its own function that simply returns, communicates intent more clearly to most readers. Use your judgment about your team's familiarity with the construct.

Key Takeaways

A loop's else clause runs only if the loop completes WITHOUT hitting a break — read it mentally as 'no-break', not as a generic 'otherwise'.

An empty iterable, or a while condition false from the start, still counts as 'completed without break' — else still runs even though the loop body never executed.

continue never suppresses else; only break does. An uncaught exception skips else too, since the loop never completes at all.

The classic use case is search-until-found: break when found, let else cleanly handle 'not found' without a separate flag variable.

Because the naming is widely considered non-intuitive, use it selectively — prefer it where it clearly shortens search logic, and prefer an explicit flag or a return-based function when clarity matters more than brevity.

for item in []: process(item) else: print('list was empty') # misreading: assumes else means 'loop ran zero times'

else on a loop is not a test for 'the loop had zero iterations' — it happens to run in that case only because no break occurred, which is trivially true when there was nothing to break from. Relying on that coincidence to mean 'empty' is a misunderstanding.

for item in items: if condition(item): break process(item) else: handle_not_found() # then separately checking a leftover flag variable too -- redundant for item in items: if condition(item): break process(item) else: handle_not_found()

Once you're using for...else correctly, there's no need for an additional flag variable to track whether break fired — that's precisely the redundancy else is meant to eliminate.

State the precise rule for when a loop's else clause executes.

The else clause runs if and only if the loop completes without a break statement executing — regardless of whether the loop ran many times, once, or zero times.

Why does for x in []: ... else: print('ran') still print 'ran', even though the loop body never executed?

Because 'completed without break' is trivially true when there's nothing to iterate over — no break occurred, so the else clause's condition for running is satisfied, even though it has nothing to do with whether the body actually ran.

Does a continue statement inside the loop body prevent the else clause from running? Why or why not?

No. continue only affects the current iteration, moving on to the next one; it has no bearing on whether the loop eventually completes normally or hits a break, so it never suppresses else.

What happens to a loop's else clause if an uncaught exception is raised inside the loop body?

The else clause is skipped, because the exception propagates out of the loop entirely — the loop never reaches a normal completion state (it neither finishes all iterations nor hits break; it's interrupted by an entirely different mechanism).

What is the classic use case that makes for...else genuinely useful, and what did people write before it existed?

Search-until-found-or-exhausted patterns: break as soon as the target is found, else handles the not-found case. Before for...else, this typically required a separate boolean flag variable set inside the loop and checked afterward.

Why do many style guides recommend caution or restraint when using loop else clauses?

Because the keyword else strongly suggests 'the opposite of the loop condition being true' by analogy with if/else, which is not what it actually means — this mismatch between the name and the real rule (about break, not about the condition) frequently confuses readers unfamiliar with the construct.

What does this print, and why does the else clause behave the way it does here?

Prints 0 1 2 3 4, then 'else ran'. The condition i == 10 is never true for any value produced by range(5), so break never executes, and the loop completes normally -- triggering else.

Write a function all_positive(numbers) that prints 'all positive' if every number in the list is positive, or the first non-positive number found, using for...else (no separate flag variable).

def all_positive(numbers: list[int]) -> None: for n in numbers: if n <= 0: print(f'Found non-positive: {n}') break else: print('all positive') all_positive([1, 2, 3]) # all positive all_positive([1, -2, 3]) # Found non-positive: -2

The loop breaks as soon as a non-positive number is found, printing it and skipping else; if the loop runs to completion (every number is positive), no break fires, so else runs and prints the 'all positive' message.

What does this print?

Prints 0, 1, 2, then 'while-else ran'. The while loop exits because count < 3 becomes False naturally -- no break occurred -- so the else clause runs.

Write a function find_divisor(n, candidates) that searches candidates for the first number that evenly divides n, printing it if found, or printing 'no divisor found' otherwise, using for...else.

def find_divisor(n: int, candidates: list[int]) -> None: for c in candidates: if c != 0 and n % c == 0: print(f'{c} divides {n}') break else: print('no divisor found') find_divisor(20, [3, 6, 7]) # no divisor found find_divisor(20, [3, 5, 7]) # 5 divides 20

Each candidate is tested in order; as soon as one evenly divides n, the loop breaks and reports it, skipping else. If none of the candidates divide n, the loop runs to completion and else reports the failure -- with no separate flag variable needed.

for i in range(5):
    if i == 10:
        break
    print(i)
else:
    print('else ran')
count = 0
while count < 3:
    print(count)
    count += 1
else:
    print('while-else ran')