Loop else Clauses
Concepts covered: Loop else
The grid search in the previous section kept found_row and found_col for 2 jobs: holding where the match sat, and telling the code after the loops whether a match happened at all. Python has a clause for the second job. A for or while loop can carry an else block, and the block runs only when the loop ends without a break.
numbers = [1, 3, 5, 7, 9]
for num in numbers:
if num % 2 == 0:
print("Found even:", num)
break
else:
print("No even numbers found")
print()
numbers2 = [1, 3, 4, 7, 9]
for num in numbers2:
if num % 2 == 0:
print("Found even:", num)
break
else:
print("No even numbers found")No even numbers found Found even: 4
Read the else as "no break". In the first search no number is even, the loop runs out of items, and the else prints its message. In the second, the 4 triggers break, which skips the else along with the rest of the loop. The clause lines up with for because it belongs to the loop.
| How the loop ended | Does else run? |
|---|---|
| It ran out of items | Yes |
| A `break` fired | No |
| The list was empty, so the body never ran | Yes |
| Some steps took `continue`, then items ran out | Yes |
| A `while` condition turned false | Yes |
The empty-list row catches people out. A loop over [] finishes at once without a break, so its else runs. For a search that is the right answer: nothing in an empty list matched, and the not-found branch should fire.
An else in the wrong column
Indentation decides which statement owns an else. Indent it under the if inside the loop and it becomes that if's branch, which runs for every item that fails the test, before the search has finished.
codes = ["BR", "SE", "JP"]
for code in codes:
if code == "SE":
print("found SE")
break
else:
print("no SE")Output:
no SE found SE
The program reports SE missing and then reports it found. Moving else: 4 spaces to the left attaches it to the for, and the output shrinks to found SE.
while loops take else too
A while loop takes an else under the same rule: the block runs when the condition turns false and is skipped when a break ends the loop. That fits retry logic, where a job tries a load up to 3 times, stops early on success, and reports failure only after using every attempt. The outcomes below stand in for 3 load attempts.
outcomes = ["timeout", "timeout", "timeout"]
attempt = 0
while attempt < 3:
result = outcomes[attempt]
attempt += 1
if result == "ok":
print("loaded on attempt", attempt)
break
else:
print("gave up after", attempt, "attempts")Output:
gave up after 3 attempts
Change the second outcome to "ok" and the program prints loaded on attempt 2 and never reaches the else.
Leaving 2 loops at once
A loop else can also replace the found-variable from the grid search. Give the inner loop an else: continue and put a break straight after it. When the inner loop runs out of items, its else sends the outer loop on to the next line. When the inner loop breaks, the else is skipped and the break below it ends the outer loop too. The outer loop's own else reports a grid with no match.
readings = [
[40, 41, 39],
[42, 120, 44],
[38, 37, 36],
]
for i, line in enumerate(readings):
for j, reading in enumerate(line):
if reading > 100:
print("spike at line", i, "position", j)
break
else:
continue
break
else:
print("no spikes")Output:
spike at line 1 position 1
Both versions give the same result. A found-variable reads more plainly to someone who has never met a loop else, so match whichever style the surrounding code already uses.
No batch arrived today, so batches is empty. What does the program print?
batches = []
for batch in batches:
if batch == "bad":
print("stopping at bad batch")
break
else:
print("all batches clean")
print("done")- done only, as an empty loop skips its else
- all batches clean, then done
- stopping at bad batch, then done
- Nothing, as looping over an empty list fails
One check that uses all 5 tools
The program below checks hourly row counts for 3 tables. zip pairs each table name with its row of counts, enumerate supplies the hour, the inner loop stops at the first empty hour, its else marks tables that never had one, and an .items() loop prints the status dict.
tables = ["orders", "users", "refunds"]
hourly = [
[410, 395, 402],
[88, 0, 91],
[15, 12, 18],
]
status = {}
for table, counts in zip(tables, hourly):
for hour, rows in enumerate(counts):
if rows == 0:
status[table] = "empty at hour " + str(hour)
break
else:
status[table] = "ok"
for table, state in status.items():
print(table, state)Output:
orders ok users empty at hour 1 refunds ok
In that program the inner loop's else sits in the same column as for hour, rows in enumerate(counts):, 4 spaces in from the outer for. Moved 4 spaces left, it would belong to the outer loop and run once, after every table had been checked.
About this lesson
This section is part of the Loops: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.