Python Part I · Module 14 · Errors and Exception Handling
Handling Several Different Errors
One try block can fail in more than one way, and the ways are rarely equivalent. A log file that does not exist means the simulation never started; a log file full of dashes where numbers should be means it started and something else went wrong. Treating those two as the same failure throws away the most useful thing you know. Python lets you attach several handlers to one block, or group types that share an answer — and the mechanism has one ordering rule that will quietly make a handler unreachable if you get it the wrong way round, with no warning of any kind.
Intermediate15 min readPythonPart IFundamentalsErrorsExceptions
Part I · Module 14 · Chapter 14.5 · Handling Several Different Errors
1. One Block, Several Ways to Fail
Parsing a single field can fail in more than one way, and the right response differs:
def runtime_of(record):
return float(record["runtime_s"])That one line can raise a KeyError (no such key), a TypeError (the value was
None) or a ValueError (the value was "N/A"). Those are three different
stories about the data, and Chapter 14.4's test — what would I actually do? —
has three different answers.
2. Several except Clauses
Attach as many as you need. Python tries them in order and runs at most one:
def runtime_of(record):
try:
return float(record["runtime_s"])
except KeyError:
return "no runtime field at all"
except ValueError:
return "field present but not a number"
except TypeError:
return "field present but not text"
print("A:", runtime_of({"runtime_s": "182.4"}))
print("B:", runtime_of({"test": "smoke"}))
print("C:", runtime_of({"runtime_s": "N/A"}))
print("D:", runtime_of({"runtime_s": None}))A: 182.4
B: no runtime field at all
C: field present but not a number
D: field present but not textFour inputs, four distinct outcomes, and each message says something genuinely different about the record. At most one clause runs — once a handler matches, the others are skipped entirely.
3. One Tuple, When the Answer Is the Same
When several types deserve the same response, group them:
def runtime_or_none(record):
try:
return float(record["runtime_s"])
except (KeyError, ValueError, TypeError):
return None
for r in [{"runtime_s": "182.4"}, {"test": "smoke"}, {"runtime_s": "N/A"}, {"runtime_s": None}]:
print(runtime_or_none(r))182.4
None
None
NoneThe parentheses are required — it is one clause naming a tuple of types, not three clauses.
Use a tuple when the types share an action. Use separate clauses when they need different ones.
The decision is not stylistic. If you find yourself writing three clauses with identical bodies, the tuple says "these are the same case" and is clearer. If you write a tuple and then branch inside the handler on which type arrived, you wanted separate clauses.
4. Order Matters, and Getting It Wrong Is Silent
Clauses are tested top-down, and the first match wins. Since a clause catches its type and every subclass, a broad clause placed above a narrow one makes the narrow one unreachable:
def wrong_order(field):
try:
return int(field)
except Exception:
return "generic"
except ValueError:
return "specific -- never reached"
print(wrong_order("12"))
print(wrong_order("n/a"))12
genericValueError is a subclass of Exception, so the first clause matches
everything the second one wanted. The second clause is dead code. Python
issues no warning: the file is valid, it imports, it runs, and a handler you
wrote is simply never used.
Reverse the two and the specific case comes back:
def right_order(field):
try:
return int(field)
except ValueError:
return "specific"
except Exception:
return "generic"
print(right_order("12"))
print(right_order("n/a"))
print(right_order(["12"]))12
specific
genericThe third call passes a list, raising TypeError, which ValueError does not
match — so it falls through to the general clause. That is the arrangement you
want: specific first, general last.
5. A Clause Catches Subclasses
That ordering rule follows from one fact worth seeing directly:
print(ValueError.__mro__)(<class 'ValueError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)__mro__ is the chain of types a ValueError is. Reading it: a ValueError
is also an Exception, which is also a BaseException. So except Exception
catches it, and so does except BaseException.
This is the mechanism, and Chapter 14.8 uses it deliberately — defining your own
error types so that one except can catch a whole family of related failures
while leaving everything else alone.
6. Handling Some and Re-Raising Others
Naming a type does not commit you to handling it. The canonical form for this chapter deliberately catches a group and then re-raises it:
def parse_field(record, key):
try:
return int(record[key])
except ValueError:
return 0
except (TypeError, KeyError):
raiseA bare raise inside a handler re-raises the exception that is currently
being handled, with its original traceback intact. So this function says:
- a malformed number is routine — substitute
0; - a missing key or a wrong type is not routine — let it propagate.
Why write a clause at all if it just re-raises? Because it is documentation that the author considered those cases and decided they are bugs rather than data problems. A reader can see the decision.
It also does real work the moment you add anything to it:
def parse_field(record, key):
try:
return int(record[key])
except ValueError:
return 0
except KeyError:
print(f" note: record has keys {sorted(record)}")
raise
print("A:", parse_field({"seed": "41"}, "seed"))
print("B:", parse_field({"seed": "n/a"}, "seed"))
print("C:", parse_field({"seed": "41"}, "sed"))A: 41
B: 0
note: record has keys ['seed']
Traceback (most recent call last):
File "reraise.py", line 13, in <module>
print("C:", parse_field({"seed": "41"}, "sed"))
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "reraise.py", line 3, in parse_field
return int(record[key])
~~~~~~^^^^^
KeyError: 'sed'The note printed, and then the program still died with the full traceback pointing at the real line. You added context without taking away any of Chapter 14.4's four free things. That combination — annotate, then re-raise — is one of the most useful patterns in the module.
Note the traceback points at line 3, where the KeyError actually happened, not
at the raise on line 8. That is what "with its original traceback" means.
7. Choosing the Granularity
Three handlers for three types is right when the three mean different things. It is over-engineering when they do not. A practical ladder:
| Situation | Shape |
|---|---|
| One failure, one answer | a single except SomeError |
| Several failures, same answer | one tuple clause |
| Several failures, different answers | several clauses, specific first |
| Some routine, some bugs | handle the routine, re-raise the rest |
| You cannot name what might fail | you are not ready to catch it |
That last row is Chapter 14.4's judgement in one line, and it is the reason
except Exception as a first resort is nearly always wrong — it is a claim
that you can handle every failure, which you have not checked.
8. Predict, Run, Observe
def classify(x):
try:
return 100 / int(x)
except ValueError:
return "not a number"
except ZeroDivisionError:
return "zero"
except TypeError:
return "wrong type entirely"
for value in ["4", "0", "n/a", None, "100"]:
print(f"{str(value):>6} -> {classify(value)}") 4 -> 25.0
0 -> zero
n/a -> not a number
None -> wrong type entirely
100 -> 1.0"4" — no failure at all.
"0" — int("0") succeeds; the division fails. Note the failing operation
is not the one you might expect from the input.
"n/a" — int fails first, so the division never happens.
None — int(None) raises TypeError, matching the third clause.
Each clause is reachable, which is the test for whether your ordering is right. If you can construct no input that reaches a clause, it is dead.
9. Exercises
Exercise 1 — three stories. Write a function reading a field from a record that distinguishes missing key, non-numeric value and wrong type, returning a different message for each.
Exercise 2 — tuple or clauses. Rewrite Exercise 1 with a single tuple clause. State in one sentence which version you would keep and why.
Exercise 3 — make a handler dead. Deliberately place except Exception
above except KeyError and prove the second never runs. Note that Python said
nothing.
Exercise 4 — the chain. Print __mro__ for KeyError, IndexError and
ZeroDivisionError. Which two share a parent that is not Exception?
Exercise 5 — annotate and re-raise. Write a handler that prints the available
keys and then re-raises. Confirm the traceback still points at the original
line, not at your raise.
Exercise 6 — reachability. For a function with three clauses, construct one input that reaches each. If you cannot, say what that proves.
Exercise 7 — order independence. Write two clauses whose order genuinely does not matter and explain why, referring to Section 5.
Exercise 8 — the ladder. For each, pick a row from Section 7's table: (a) a
config file that may be missing or malformed (b) six numeric fields any of which
may be N/A (c) a tool that may be absent, may time out, or may return garbage.
10. Summary
- One
trycan have severalexceptclauses, and at most one of them runs. - A tuple of types is one clause for several types that share an action.
- Separate clauses are for types that need different actions; branching inside a tuple handler means you wanted separate clauses.
- Clauses are tested top-down and the first match wins.
- A clause catches its type and all subclasses, which is why a broad clause above a narrow one makes the narrow one dead code.
- Python never warns about an unreachable handler — the ordering is yours to get right.
- Order by specificity, most derived first. Unrelated types can go in any order.
__mro__shows the chain that decides what a clause will catch.- A bare
raisere-raises the current exception with its original traceback. - Annotate-then-re-raise adds context without giving up the crash.
The next chapter adds the two clauses that are not about catching anything — one that runs only on success, and one that runs no matter what.
Where this fits
Part of the Python curriculum.
