Python Part I · Module 14 · Errors and Exception Handling
Why except: pass Destroys a Regression
This module has argued that crashing is a feature, that a handler must have something real to do, and that an error type is worth naming. This chapter is the counter-example that makes all of it concrete, and it is not a hypothetical — it is the single most common serious defect in automation code, it takes two lines to write, it passes code review because it looks defensive, and the damage it does is invisible by construction. The script does not get slower or noisier. It gets quieter, and it starts reporting success.
Intermediate16 min readPythonPart IFundamentalsErrorsExceptions
Part I · Module 14 · Chapter 14.9 · Why except: pass Destroys a Regression
1. The Two Lines
try:
...
except: # never do this
passIt looks careful. It reads as "this part might fail, and I have thought about it". What it actually says is: whatever goes wrong here, discard all evidence that it happened and continue as though it did not.
There are two separate mistakes stacked on one another, and they are worth separating:
except:with no type catches absolutely everything, including things that are not errors at all (Section 4);passdestroys the exception object, its message and its traceback without recording anything (Sections 2 and 3).
Either is bad. Together they are the canonical disaster.
2. A Green Nightly on a Red Run
This is a regression summariser. The log has four lines, one test failed, and a
FAIL line carries no runtime column because there was no run to time:
LOG = [
"smoke PASS 182.4",
"cdc PASS 204.1",
"reset FAIL",
"burst PASS 199.8",
]
def status_of(line):
"""The second field is the status, the third is the runtime."""
fields = line.split()
return fields[1], float(fields[2])
def count_failures(lines):
failures = 0
for line in lines:
try:
status, _runtime = status_of(line)
if status == "FAIL":
failures += 1
except:
pass
return failures
print("failures reported:", count_failures(LOG))
print("failures actually present:", 1)failures reported: 0
failures actually present: 1The nightly is green. The one line describing a real failure is also the one
line status_of cannot parse, so it raised an IndexError, so it was silently
skipped — and skipping it removed the only evidence that anything failed.
This is not a contrived coincidence. It is the normal shape of the bug: malformed lines and interesting lines are correlated, because the thing that went wrong is also what made the output unusual. A crashed simulation truncates its log. A timed-out tool writes no summary. A licence failure produces an empty report. The lines your parser chokes on are disproportionately the lines that matter.
Without the second print — which no real script contains — there is nothing
at all to see. The output is a number, it is plausible, and it is wrong.
3. What the Handler Threw Away
The same function with the handler removed:
LOG = [
"smoke PASS 182.4",
"reset FAIL",
]
def status_of(line):
fields = line.split()
return fields[1], float(fields[2])
for line in LOG:
print(status_of(line))('PASS', 182.4)
Traceback (most recent call last):
File "loud.py", line 13, in <module>
print(status_of(line))
~~~~~~~~~^^^^^^
File "loud.py", line 9, in status_of
return fields[1], float(fields[2])
~~~~~~^^^
IndexError: list index out of rangeEverything needed to fix this in two minutes is in that output: the type, the
exact expression (fields[2], via the caret line), the function, and the fact
that it happened on the second line. The except: pass version had all of this
and deleted it.
Chapter 14.4 Section 6 listed four things a crash gives you for free. This construct discards all four simultaneously, which is why it is worse than any single bug it might be hiding.
4. A Bare except: Catches Things That Are Not Errors
The second mistake is subtler. except: with no type does not mean "catch all
errors" — it means catch all BaseException, which includes several things that
are not errors:
import sys
def guarded():
try:
sys.exit(3)
except Exception:
return "except Exception caught it"
except BaseException as e:
return f"only BaseException caught it: {type(e).__name__}"
print(guarded())only BaseException caught it: SystemExitsys.exit(3) works by raising SystemExit, which inherits from
BaseException but not from Exception. So:
except Exception:lets it through — correct;except:swallows it, and your script ignores its own request to exit.
KeyboardInterrupt — Ctrl-C — is in the same position. A bare except: inside
a loop produces a script that cannot be interrupted from the keyboard: every
Ctrl-C is caught and discarded, and the loop continues.
It also destroys the exit status. Chapter 14.4 Section 6 established that a
non-zero exit status is how a wrapper, a Makefile or a CI job learns the run
failed. Swallowing SystemExit means the script exits 0 — and the flow around
it concludes everything is fine.
5. Four Ways to Swallow, Ranked
The bare except: pass is the famous one, but it is not the worst:
| Construct | Catches | Leaves behind |
|---|---|---|
except ValueError: pass | one type | nothing, but only for that type |
except Exception: pass | every error | nothing |
except: pass | errors, SystemExit, Ctrl-C | nothing, and breaks exit status |
return inside finally | everything in flight | nothing, and contains no except at all |
The last row is Chapter 14.6 Section 6, and it is the most dangerous because
nothing in it looks like exception handling. A reviewer scanning for except
will not find it.
Even the first row is a real risk when the type is broader than you think.
except ValueError: pass around a block containing int(), float() and
list.index() is swallowing three unrelated failures — as Chapter 14.6
Section 2 demonstrated.
6. When a Broad Handler Is Legitimate
There are two honest uses, and both record rather than discard.
A top-level reporter that converts any unexpected failure into a clean message and a non-zero exit:
import sys
def main(records):
total = 0.0
for r in records:
total += float(r["runtime_s"])
return total
try:
print("total:", main([{"runtime_s": "182.4"}, {"runtime_s": "204.1"}]))
print("total:", main([{"test": "cdc"}]))
except Exception as e:
sys.exit(f"run failed: {type(e).__name__}: {e}")total: 386.5
run failed: KeyError: 'runtime_s'The failure is reported with its type and message, and the script exits with status 1 so automation knows. Nothing is hidden; the traceback is traded for a one-line summary, deliberately, at the outermost level.
A loop that must finish and reports what it skipped:
LOG = [
"smoke PASS 182.4",
"reset FAIL",
"burst PASS 199.8",
]
def status_of(line):
fields = line.split()
return fields[1], float(fields[2])
def count_failures(lines):
failures = 0
skipped = []
for line in lines:
try:
status, _runtime = status_of(line)
except (IndexError, ValueError):
skipped.append(line)
continue
if status == "FAIL":
failures += 1
return failures, skipped
failures, skipped = count_failures(LOG)
print("failures:", failures)
print("unparseable lines:", len(skipped))
for line in skipped:
print(" could not parse:", line)failures: 0
unparseable lines: 1
could not parse: reset FAILfailures: 0 is still wrong — and now it is impossible to miss, because the
line underneath says one line could not be parsed and prints it. A human reading
this output sees the problem in one second. A wrapper can refuse to call the run
green while len(skipped) is non-zero.
A handler that drops data must report how much it dropped. That one rule converts a silent corruption into a visible defect.
7. The Repair, in Three Parts
Whenever you find an except: pass, apply all three:
- Narrow the type. Name what you actually expect —
except (IndexError, ValueError). If you cannot name it, you have not understood the failure yet, which is Chapter 14.5 Section 7's last row. - Do something real. A default, a retry, a clean exit — Chapter 14.4's
test.
passis never the answer. - Count what you skipped, and print it. The count is what turns Section 2's invisible bug into Section 6's visible one.
If after all three the honest answer is still "nothing to do here", then there was no reason to catch it, and the correct fix is to delete the handler and let it crash.
8. Predict, Run, Observe
def summarise(rows):
total = 0.0
skipped = 0
for row in rows:
try:
total += float(row["runtime_s"])
except (KeyError, ValueError, TypeError):
skipped += 1
return total, skipped
ROWS = [
{"runtime_s": "182.4"},
{"test": "cdc"},
{"runtime_s": "N/A"},
{"runtime_s": None},
{"runtime_s": "199.8"},
]
total, skipped = summarise(ROWS)
print(f"total: {total:.1f}")
print(f"rows: {len(ROWS)} counted: {len(ROWS) - skipped} skipped: {skipped}")
if skipped:
print("WARNING: summary is incomplete")total: 382.2
rows: 5 counted: 2 skipped: 3
WARNING: summary is incompletetotal: 382.2 is a real number and a useless one — it is the sum of two of
the five rows. Printed on its own, it looks exactly like a runtime total for the
whole run, and nothing about it suggests otherwise.
Three rows failed for three different reasons: a missing key, an unparseable
string and a None. The tuple clause treats them identically, which is correct
here because the action is the same — but the count is what preserves the fact
that they happened.
The second line is what makes the first one safe. Two of five counted is obviously wrong to a human and testable by a wrapper.
The WARNING fires because skipped is truthy — Chapter 9.5's rule, used
deliberately. Three is truthy, zero is not, so a clean run prints nothing extra
and a degraded one announces itself.
One detail worth keeping: total is formatted with :.1f. Without it the
output reads 382.20000000000005, because repeatedly adding floats accumulates
representation error — exactly what Chapter 4.13 was about. A summary line is
precisely where that leaks into a report.
9. Exercises
Exercise 1 — the green nightly. Reproduce Section 2. Change the FAIL line
so it has a runtime column and confirm the count becomes 1. Explain in one
sentence why the original was so dangerous.
Exercise 2 — what was thrown away. Remove the handler from your Section 2 copy and list every fact the traceback gives you that the silent version did not.
Exercise 3 — the uninterruptible loop. Write a loop with a bare except:
and a long range, and try to stop it with Ctrl-C. Then change it to
except Exception: and try again.
Exercise 4 — exit status. Write a script that calls sys.exit(3) inside a
bare except: block and check its exit status from the shell. Say what a CI
job would conclude.
Exercise 5 — the ranking. Write all four constructs from Section 5 around the same failing operation. For each, say what a reviewer scanning the file would notice.
Exercise 6 — the three-part repair. Take Section 2's function and apply all three steps. Show the output before and after.
Exercise 7 — the count. Add a skipped-line count to any parser you have written in this module. Then decide what a wrapper script should do when the count is non-zero.
Exercise 8 — the unformatted total. Remove the :.1f from Section 8 and
record what prints. Explain why a skipped-row count and a float-formatting bug
are both reasons not to trust a bare summary number.
10. Summary
except: passdiscards the error, its message and its traceback, and continues as though nothing happened.- The skipped line is often the line that mattered — malformed output and interesting output are correlated, because the failure is what made it unusual.
- A silent handler turns a broken parser into a green nightly, and the output stays plausible.
- It discards all four things a crash gives you: the stop, the what, the where, and the non-zero exit status.
- A bare
except:catchesBaseException, includingSystemExitandKeyboardInterrupt— so it can make a script uninterruptible and destroy its exit status. - If you must catch broadly, catch
Exception, never bareexcept:. returninsidefinallyis worse and contains noexceptat all.- A broad handler is legitimate when it records: a top-level reporter that exits non-zero, or a loop that counts what it skipped.
- A handler that drops data must report how much.
- The repair is three parts: narrow the type, do something real, count the skips — and if there is nothing to do, delete the handler.
That closes Module 14. You can classify a failure, read it, catch the ones you can act on, raise your own when the input makes no sense, and recognise the handler that would hide all of it. The next module opens the files that most of these errors have been about.
Where this fits
Part of the Python curriculum.
