Skip to content
VLSI Mentor

Python Part I · Module 14 · Errors and Exception Handling

Exceptions: What They Are and When to Let Them Escape

Three chapters of this module have let every failure terminate the program, and it is worth noticing how well that worked. Each crash arrived with a precise description of what went wrong and where, and none of them lied. This chapter introduces the statement that intercepts a failure instead — and then spends most of its length on the harder question, which is when you should. The syntax takes five minutes. The judgement separates a script that reports what actually happened from one that prints a reassuring summary over the top of a broken run, and the industry is full of the second kind.

Intermediate17 min readPythonPart IFundamentalsErrorsExceptions

Part I · Module 14 · Chapter 14.4 · Exceptions: What They Are and When to Let Them Escape

1. An Exception Is an Object

Everything on the last line of a traceback comes from an object. When something goes wrong, Python creates an exception object carrying a type and a message, and then raises it — which means normal execution stops and the object travels up the call stack looking for something to handle it.

If nothing does, Python prints the traceback and exits. That is what every example so far has done.

The object is ordinary and you can inspect it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try:
    int("n/a")
except ValueError as e:
    print("type:", type(e).__name__)
    print("str :", e)
    print("args:", e.args)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
type: ValueError
str : invalid literal for int() with base 10: 'n/a'
args: ("invalid literal for int() with base 10: 'n/a'",)

e is just a value. type(e).__name__ is the name you read in Chapter 14.3, and str(e) — what print shows — is the message half. Nothing exotic is happening; this is the same attribute access Chapter 13.1 used on modules.

2. The Statement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try:
    # code that might fail
except SomeError as e:
    # what to do if it does

Two things to read carefully:

  • as e is optional. Use it when you want the message; omit it when the type alone tells you everything.
  • The except clause names a type. It catches that type and its subclasses, and nothing else.

3. What Happens to the Rest of the try Block

This is the part that surprises people. The moment something raises, the rest of the try block is abandoned — not resumed, not retried:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try:
    print("1 before")
    int("n/a")
    print("2 never runs")
except ValueError:
    print("3 handler")
print("4 after")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 before
3 handler
4 after

2 never runs is absent. Execution jumps from the failing expression straight to the handler, and then continues after the whole try statement.

That jump is why a try block should be short. Everything inside it is code you are declaring might be skipped, and a twenty-line try block is twenty lines of uncertainty about how much of it happened.

4. A Handler That Earns Its Place

Here is a genuinely good use. Some fields in a report are N/A, and the sensible reading of N/A is "no runtime recorded":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def runtime_or_zero(field):
    try:
        return float(field)
    except ValueError:
        return 0.0


for raw in ["182.4", "204.1", "N/A", ""]:
    print(f"{raw!r:>8} -> {runtime_or_zero(raw)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 '182.4' -> 182.4
 '204.1' -> 204.1
   'N/A' -> 0.0
      '' -> 0.0

What makes this good is not the syntax. It is that the handler has a real answer: a missing runtime is zero, that decision is correct for this report, and a reader can see it in one line.

5. The Type Has to Match

An except clause catches one family of exception. Anything else passes straight through as though the try were not there:

Azvya Education Pvt. Ltd.VLSI Mentor
wrongtype.py (BROKEN ON PURPOSE)
def only_value():
    try:
        return 10 / 0
    except ValueError:
        return "handled"


print("about to fail")
print(only_value())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
about to fail
Traceback (most recent call last):
  File "wrongtype.py", line 9, in <module>
    print(only_value())
          ~~~~~~~~~~^^
  File "wrongtype.py", line 3, in only_value
    return 10 / 0
           ~~~^~~
ZeroDivisionError: division by zero

The try caught nothing because ZeroDivisionError is not a ValueError. This is a feature: a handler written for one failure does not silently absorb a different one. Chapter 14.5 is about handling several deliberately.

6. Crashing Is a Feature

Here is the claim this chapter is built on. An uncaught exception does four things, all of them valuable:

  1. It stops the program before the bad value can be used by anything else.
  2. It prints exactly what went wrong, with a type and a message.
  3. It prints where, with the full call chain.
  4. It sets a non-zero exit status, which is how any wrapper script, Makefile or CI job knows the run failed.

That is a complete bug report, generated automatically, every time, for free. A try/except that cannot do something better than that is a net loss — it throws away all four.

7. The Test for a Handler

Before writing except, answer one question:

What would I actually do in the handler?

There are only a few good answers:

  • Substitute a known-correct default, as Section 4 does with 0.0.
  • Try a different source — another path, another format, a fallback.
  • Add context and stop, so the operator gets a sentence instead of a stack.
  • Record it and continue deliberately, where "continue" is genuinely right and the record is genuinely read.

And one bad answer, which accounts for most handlers in the wild:

  • "Print something and carry on as though it worked."

If that is your answer, there is no handler worth writing. The script will produce output that looks like a result, is not one, and nobody will know. Chapter 14.9 is entirely about what that costs.

8. Catch What You Can Act On

Compare two versions of the same parser. The first catches a failure it has a real answer for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def parse_runtimes(lines):
    """A line without a numeric runtime is reported as having none."""
    runtimes = {}
    for line in lines:
        name, _, raw = line.split()
        try:
            runtimes[name] = float(raw)
        except ValueError:
            runtimes[name] = None
    return runtimes


LOG = ["smoke PASS 182.4", "cdc PASS 204.1", "reset FAIL N/A"]
parsed = parse_runtimes(LOG)
recorded = 0
for name, value in parsed.items():
    print(f"{name:<8} {value}")
    if value is not None:
        recorded += 1
print("recorded:", recorded, "of", len(parsed))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke    182.4
cdc      204.1
reset    None
recorded: 2 of 3

The handler has an answer (None means "not recorded"), and — critically — the script still reports the distinction on the last line. Two of three.

Now the version that catches something it cannot act on:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def parse_runtimes_bad(lines):
    """Catches a malformed LINE, and has nothing sensible to do about it."""
    runtimes = {}
    for line in lines:
        try:
            name, _, raw = line.split()
            runtimes[name] = float(raw)
        except ValueError:
            pass
    return runtimes


LOG = ["smoke PASS 182.4", "cdc PASS 204.1", "reset FAIL"]
print(parse_runtimes_bad(LOG))
print("tests in log:", len(LOG), "-- tests reported:", len(parse_runtimes_bad(LOG)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'smoke': 182.4, 'cdc': 204.1}
tests in log: 3 -- tests reported: 2

The dictionary looks perfectly healthy. Nothing in it indicates that a third test existed, and reset — which failed — has vanished from the report entirely. The only reason you can see the problem here is the second line, which a real script would not print.

Note the subtlety: the widened try in the second version now covers the line.split() unpacking as well, so one handler is absorbing two unrelated failures. That is what Section 3's "keep the block short" is protecting you from.

9. Adding Context and Stopping

When a failure is fatal but the traceback would not help the person running the script, the right move is a clear message and a non-zero exit:

Azvya Education Pvt. Ltd.VLSI Mentor
loadcfg.py
import sys


def load_seed(raw):
    return int(raw)


def main():
    try:
        seed = load_seed("default")
    except ValueError as e:
        sys.exit(f"bad config: seed must be an integer ({e})")
    print("seed:", seed)


main()
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bad config: seed must be an integer (invalid literal for int() with base 10: 'default')

sys.exit(message) prints the message and exits with status 1, which is how a wrapper knows the run failed. Three properties make this legitimate where Section 8's bad version was not:

  1. the program stops — nothing downstream runs on a bad seed;
  2. the message names the cause, and includes the original text via {e};
  3. the exit status is non-zero, so automation sees the failure.

That is strictly more useful than a traceback for this audience — an operator reading a log — and strictly less useful for a developer, which is the trade you are making. Make it only where the cause is genuinely understood.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def attempt(value):
    try:
        print("  try:", 100 / value)
        print("  still in try")
    except ZeroDivisionError:
        print("  handler")
    print("  after")
    return "done"


print("A:", attempt(4))
print("B:", attempt(0))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  try: 25.0
  still in try
  after
A: done
  handler
  after
B: done

A — no exception, so the handler is skipped entirely and both try lines run.

B — still in try is missing. The failure abandoned the rest of the block.

Both return done. Once a handler has run, the function continues normally; an exception that has been caught leaves no trace in the control flow after the try statement.

after printed in both cases, which is the point people most often get wrong: code after the try statement is not part of it.

11. Exercises

Exercise 1 — the object. Catch a KeyError and print its type name, its str() and its args. Say which part you would put in a message to a colleague.

Exercise 2 — the abandoned block. Write a try block with four prints and a failure in the middle. Predict which prints appear, then run it.

Exercise 3 — a handler that earns it. Write a function converting a field to a float, returning a documented default for unparseable input. Then write one sentence justifying the default to a reviewer.

Exercise 4 — the wrong type. Write a try that catches ValueError around code that raises KeyError. Confirm the traceback is identical to having no try at all.

Exercise 5 — the four free things. Take any failing script from Chapter 14.3 and list, concretely, the four things its crash gave you. Then wrap it in except Exception: print("failed") and list what remains.

Exercise 6 — count what you dropped. Modify Section 8's bad parser so it also reports how many lines it skipped. Say why that single number changes the handler from dishonest to acceptable.

Exercise 7 — exit status. Write a script that exits via sys.exit("message") and check its exit status from the shell. Explain why a wrapper script cares.

Exercise 8 — the test. For each, say whether you would catch it and what the handler would do: (a) a missing optional field (b) a malformed number in the one line you need (c) a missing simulator binary (d) a divide by zero when counting an average over an empty list.

12. Summary

  • An exception is an object with a type and a message, travelling up the call stack until something handles it.
  • try/except intercepts it; as e binds the object when you need the message.
  • The rest of the try block is abandoned on failure — so keep the block short, covering only what you mean to guard.
  • An except clause catches one type and its subclasses. Anything else propagates as though the try were absent.
  • Crashing is a feature: it stops the program, says what and where, and sets a non-zero exit status.
  • A handler must beat all four of those to be worth writing.
  • The test is "what would I actually do?" — a default, another source, or a clean exit with a message.
  • "Print something and carry on" is not an answer; it manufactures a result that was never computed.
  • A handler that drops data should report how much it dropped.
  • sys.exit(message) prints to standard error and exits with status 1, which is how automation learns the run failed.

The next chapter handles several different failures in one place, where a missing file and a malformed number need genuinely different answers.

Where this fits

Part of the Python curriculum.