Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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}))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 182.4
B: no runtime field at all
C: field present but not a number
D: field present but not text

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
182.4
None
None
None

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
12
generic

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
12
specific
generic

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(ValueError.__mro__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(<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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def parse_field(record, key):
    try:
        return int(record[key])
    except ValueError:
        return 0
    except (TypeError, KeyError):
        raise

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

Azvya Education Pvt. Ltd.VLSI Mentor
reraise.py (BROKEN ON PURPOSE)
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"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

SituationShape
One failure, one answera single except SomeError
Several failures, same answerone tuple clause
Several failures, different answersseveral clauses, specific first
Some routine, some bugshandle the routine, re-raise the rest
You cannot name what might failyou 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
     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 try can have several except clauses, 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 raise re-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.