Python Part I · Module 10 · Loops
Stopping Early: break, continue and the First Failure
A loop that always runs to the end of its collection is the right shape for counting things and the wrong shape for finding one. A twenty-gigabyte simulation log may contain a single UVM_FATAL on line forty, and everything after it is a consequence of that fatal rather than new information — so reading the remaining nineteen gigabytes answers nothing and costs minutes. The same loop may also want to ignore comment lines entirely, dealing only with the rows that carry data. Python has one statement for each of those: one that leaves the loop, and one that abandons the current pass. They are small and they change the shape of what a for loop can express.
Foundation16 min readPythonPart IFundamentalsLoopsControl Flow
Part I · Module 10 · Chapter 10.7 · Stopping Early: break, continue and the First Failure
1. Finding Is Not Counting
Here is a log and a question that does not need all of it:
log = [
"UVM_INFO @0: reset asserted",
"UVM_FATAL @120: bus protocol violation",
"UVM_INFO @200: shutting down",
"UVM_ERROR @210: no response",
]
for line in log:
if "UVM_FATAL" in line:
print("first fatal:", line)first fatal: UVM_FATAL @120: bus protocol violationThe answer is right, and the loop kept reading after it had it. With four lines that costs nothing. With four million it costs the whole scan, and the lines after a fatal are usually the simulator unwinding rather than independent evidence.
(As in Chapter 10.2, the list stands in for a file you would normally read. Opening one is Chapter 15.2; the loop is identical either way.)
2. break Ends the Loop
log = [
"UVM_INFO @0: reset asserted",
"UVM_FATAL @120: bus protocol violation",
"UVM_INFO @200: shutting down",
"UVM_ERROR @210: no response",
]
for line in log:
if "UVM_FATAL" in line:
print("first fatal:", line)
break
print("scan stopped")first fatal: UVM_FATAL @120: bus protocol violation
scan stoppedbreak ends the loop immediately. Execution continues at the first line after the loop body — here, print("scan stopped").
3. What break Actually Skips
Two things, and the second is the one that matters operationally:
log = ["INFO a", "UVM_FATAL b", "INFO c", "UVM_ERROR d"]
scanned = 0
for line in log:
scanned += 1
if "UVM_FATAL" in line:
first_fatal = line
break
print("checked:", line)
print("stopped after", scanned, "of", len(log), "lines")
print("first fatal:", first_fatal)checked: INFO a
stopped after 2 of 4 lines
first fatal: UVM_FATAL bchecked: INFO a printed once, not twice — so break skipped the rest of the body on the pass it ran in. And scanned reached 2, not 4 — so it also skipped the rest of the collection.
That second number is the point. The loop did half the work. On a log where the fatal is at line 40 of 4,000,000, it does 0.001% of the work, and the saving is the difference between a report that returns immediately and one that reads twenty gigabytes to tell you something it knew in the first second.
4. continue Skips the Rest of One Pass
continue does not end the loop. It abandons the current pass and goes straight to the next item:
lines = ["# header", "axi_smoke PASS", "", "# note", "axi_burst FAIL"]
kept = 0
for line in lines:
if line.startswith("#"):
continue
if not line.strip():
continue
kept += 1
print(kept, line)
print("kept", kept, "of", len(lines))1 axi_smoke PASS
2 axi_burst FAIL
kept 2 of 5Five passes happened. Three of them reached a continue and went no further; two ran the whole body. The loop still visited every item — continue is about the body, not about the collection.
The two guards read as a list of reasons to ignore a line, each stated once at the top, which is exactly how you would describe the rule in words: skip comments; skip blanks; otherwise, process it.
5. continue or an if?
The same filter can always be written as a condition around the body:
lines = ["# header", "axi_smoke PASS", "", "# note", "axi_burst FAIL"]
kept = 0
for line in lines:
if not line.startswith("#") and line.strip():
kept += 1
print(kept, line)
print("kept", kept, "of", len(lines))1 axi_smoke PASS
2 axi_burst FAIL
kept 2 of 5Same output. Which is better depends on how many reasons there are and how long the body is.
- One simple reason and a short body → the
ifis clearer. The condition says what you want, positively. - Several unrelated reasons, or a long body →
continueguards are clearer. Each exclusion is stated on its own line, and the body is not indented inside a condition that may itself be compound and negated.
The if version above already shows the strain: not ... and ... mixes a negative with a positive, and adding a third exclusion makes it worse. Chapter 9.3 warned about exactly that shape.
6. pass Does Nothing — and Is Not a Quiet continue
pass is a statement that does nothing at all. It exists because Python requires a block to contain at least one statement, as Chapter 9.2 showed when an empty if raised IndentationError:
for test in ["smoke", "reset"]:
if test == "smoke":
pass
else:
print("handling", test)
print("done")handling reset
doneThe pass branch is a deliberate "nothing to do here, and that is intentional" — more honest than omitting the branch, because a reader can see the case was considered.
It is not a continue. The same loop with each makes the difference unmissable:
print("--- continue ---")
for n in [1, 2, 3]:
if n == 2:
continue
print("body end for", n)
print("--- pass ---")
for n in [1, 2, 3]:
if n == 2:
pass
print("body end for", n)--- continue ---
body end for 1
body end for 3
--- pass ---
body end for 1
body end for 2
body end for 3continue skipped the rest of the body on the second pass, so 2 is missing. pass did nothing, so execution carried straight on and 2 printed like every other value.
7. break Exits One Loop
When a loop contains another, break ends only the loop it is written in — the innermost one:
for test in ["smoke", "reset"]:
for seed in [1, 2, 3]:
if seed == 2:
break
print(test, seed)
print("done")smoke 1
reset 1
doneThe inner loop broke at seed == 2 both times, and the outer loop carried on to its second test regardless. There is no statement that breaks out of two loops at once. Chapter 10.11 covers nesting properly, including what to do when you genuinely need to leave both.
8. Both Statements in One Scan
The canonical use, with the two doing their separate jobs:
log = [
"# simulation log",
"UVM_INFO @0: reset asserted",
"",
"UVM_ERROR @120: crc mismatch",
"UVM_FATAL @130: bus protocol violation",
"UVM_INFO @200: shutting down",
]
errors_before_fatal = 0
first_fatal = ""
for line in log:
if line.startswith("#") or not line.strip():
continue
if "UVM_FATAL" in line:
first_fatal = line
break
if "UVM_ERROR" in line:
errors_before_fatal += 1
print("errors before the fatal:", errors_before_fatal)
print("first fatal:", first_fatal)errors before the fatal: 1
first fatal: UVM_FATAL @130: bus protocol violationcontinue removes the lines that are not data. break stops the scan once the question is answered. The order of the three checks is a branch-order decision of exactly the kind Chapter 9.1 covered — the continue guards come first because a comment line should not be counted as anything.
9. break in a while Loop
break works in both statements. In a while, it is an exit that does not go through the condition:
polls = ["RUNNING", "FAILED", "DONE"]
checks = 0
while checks < 10:
state = polls[checks]
checks += 1
if state != "RUNNING":
print("settled on", state, "after", checks, "checks")
break
print("loop over")settled on FAILED after 2 checks
loop overThis is the bounded poll from Chapter 10.5 with the success exit moved out of the condition and into a break. Both forms are fine; the break version keeps the condition as the pure safety bound, which some readers find clearer.
10. Which Exit Did It Take?
Here is the problem break creates. After the loop, how do you know whether it found something or simply ran out?
tests = ["smoke", "reset", "burst"]
found = False
for test in tests:
if test == "reset":
found = True
break
print("found:", found, "| loop variable:", test)found: True | loop variable: resetThe flag works. It is also three extra lines of bookkeeping — declare it, set it, check it — for a question the loop already knows the answer to.
And the loop variable is not a reliable substitute. It holds "reset" here because the loop broke there; had nothing matched, it would hold "burst" — the last item — which looks exactly like a successful find of the last item. Chapter 10.1's Section 4 is the reason, and relying on it is a bug Chapter 10.12 returns to.
11. Predict, Run, Observe
# A
for n in [1, 2, 3, 4]:
if n == 3:
break
print("A:", n)
# B
for n in [1, 2, 3, 4]:
if n == 3:
continue
print("B:", n)
# C
count = 0
for n in [1, 2, 3, 4]:
count += 1
if n == 2:
break
print("C: passes =", count)
# D
for n in [1, 2]:
if n == 1:
pass
print("D:", n)The verified output:
A: 1
A: 2
B: 1
B: 2
B: 4
C: passes = 2
D: 1
D: 2A — stops entirely at 3, so only 1 and 2 print.
B — skips only 3; 4 still prints, because the loop continued.
C — two passes. The count += 1 on the breaking pass still ran, because it is before the break.
D — pass changed nothing; both values print.
12. Exercises
Exercise 1 — first match. Given widths = [1, 8, 0, 32], print the first width that is zero and stop. Print how many items were examined, and verify it is 3.
Exercise 2 — break or not. For each, say whether break belongs: (a) count the failing tests (b) report whether any test failed (c) find the seed of the first failure (d) total the runtimes (e) check whether a signal name appears anywhere in a netlist list.
Exercise 3 — rewrite the guards. Convert Section 4's two continue guards into one if around the body. Then add a third exclusion — skip lines shorter than five characters — to both versions, and say which stayed readable.
Exercise 4 — pass versus continue. Write a loop over [1, 2, 3] that prints every number, with if n == 2: pass in it. Then change pass to continue and predict the new output before running. Explain the difference in one sentence.
Exercise 5 — the body before the break. In Section 3, scanned reached 2. Move scanned += 1 to the line after the if block and predict the new value. Verify, and explain what that says about where break takes effect.
Exercise 6 — nested break. Using the program in Section 7, add a line inside the outer loop but after the inner one that prints finished test <name>. Predict the output, verify, and say in one sentence what it proves about break.
Exercise 7 — the flag. Write a loop that searches ["a", "b", "c"] for "z" using a found flag and a break. Print the flag and the loop variable afterwards. Explain in two sentences why the loop variable is misleading here.
Exercise 8 — combine both. Scan a list of log lines, ignoring blank lines and lines beginning with //, counting WARNING lines, and stopping at the first ERROR. Print the warning count and the error line.
13. Summary
breakends the loop immediately — skipping both the rest of the body and the rest of the collection.- The second saving is the important one: a scan that stops at the first match does a fraction of the work.
- Use
breakwhen a second match would not change your answer. continueabandons the current pass and goes to the next item. The loop still visits everything.continueguards beat one compound condition when there are several unrelated reasons to skip, or the body is long.passdoes nothing at all. It is not a quietcontinue— the rest of the body still runs.breakexits one loop, the innermost one containing it.breakworks in awhiletoo, and lets the condition stay a pure safety bound.- After a loop that may have broken, a flag tells you which exit you took — and the loop variable does not, because it holds the last item either way.
The next chapter replaces that flag with something the loop statement already provides.
Where this fits
Part of the Python curriculum.
