Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try:
    ...
except:      # never do this
    pass

It 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);
  • pass destroys 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failures reported: 0
failures actually present: 1

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
loud.py (BROKEN ON PURPOSE)
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
('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 range

Everything 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
only BaseException caught it: SystemExit

sys.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:

ConstructCatchesLeaves behind
except ValueError: passone typenothing, but only for that type
except Exception: passevery errornothing
except: passerrors, SystemExit, Ctrl-Cnothing, and breaks exit status
return inside finallyeverything in flightnothing, 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:

Azvya Education Pvt. Ltd.VLSI Mentor
toplevel.py
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}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failures: 0
unparseable lines: 1
  could not parse: reset    FAIL

failures: 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:

  1. 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.
  2. Do something real. A default, a retry, a clean exit — Chapter 14.4's test. pass is never the answer.
  3. 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total: 382.2
rows: 5  counted: 2  skipped: 3
WARNING: summary is incomplete

total: 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: pass discards 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: catches BaseException, including SystemExit and KeyboardInterrupt — so it can make a script uninterruptible and destroy its exit status.
  • If you must catch broadly, catch Exception, never bare except:.
  • return inside finally is worse and contains no except at 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.