26 · try / except / else / finally and custom exceptions
Catch errors gracefully, understand the exception hierarchy, define custom exception classes, and learn why Python culture prefers EAFP over LBYL.
Python uses **exceptions** for all error signalling. `try` wraps risky code; `except SomeError as e` catches specific errors; `else` runs only when no exception occurred; `finally` always runs — even after a `return` or another exception. Never use bare `except:` — it catches `KeyboardInterrupt` and `SystemExit` too.
Without this:
Without exception handling, any unexpected input — a corrupt batch, a missing file, a non-numeric string — crashes the whole program. With `try/except`, you can log the failure, skip the bad item, and keep the training loop running.
When Python encounters an error at runtime, it raises an exception — an object that travels up the call stack until something catches it or the program terminates. The class hierarchy starts at BaseException (which includes KeyboardInterrupt and SystemExit) and branches into Exception (the base for all regular errors) — then ValueError, TypeError, KeyError, FileNotFoundError, ZeroDivisionError, and hundreds more. The try/except/else/finally construct lets you handle errors gracefully: run risky code in try, catch specific error types in except, run success-only code in else, and run cleanup in finally regardless of outcome. You can also define your own exception classes by subclassing Exception — this is how ML libraries signal domain-specific failures like convergence errors or missing model fits.
Python (in browser)
`except ValueError as e` binds the exception object to `e` — you can inspect `str(e)` for a human-readable message and `type(e).__name__` for the class name. `except (TypeError, ZeroDivisionError) as e` catches either type in one clause.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Run this and read the output carefully. `else` only fires on the success path — if `try` raises anything, `else` is skipped entirely. `finally` fires on every path: success, `except` match, unmatched exception, and even `return`. This makes `finally` the right place to release resources (database connections, locks, temp files).
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`class ConvergenceError(Exception): pass` is all you need for a meaningful custom exception. Adding a docstring explains when it should be raised. Callers catch it with `except ConvergenceError as e` — they can distinguish it from generic `ValueError` without parsing message strings.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
EAFP is idiomatic for containers because CPython's `try/except` has near-zero overhead on the success path — the exception machinery only kicks in on failure. For expensive checks (file stat, network probe) LBYL can be more readable, but `.get()` / `.setdefault()` / `.pop(key, default)` are the real one-liner alternatives for dict access.
Lesson mml-39 covers gradient descent convergence. When the update rule fails to converge — loss exploding, NaN gradients — the right signal is a `ConvergenceError` or an early-stop flag, not a silent wrong answer. Exception design and convergence criteria are two sides of the same engineering decision.
Does the `finally` block run if a `return` statement executes inside `try`?
- `try/except SomeError as e` catches specific errors. `else` runs only on success. `finally` always runs — perfect for cleanup. Never use bare `except:`.
- Define custom exceptions with `class MyError(Exception): pass`. Raise with `raise MyError("message")`. Chain with `raise NewError("context") from original_exc`.
- Python culture prefers EAFP (`try/except`) over LBYL (`if key in d`). For dict defaults, `d.get(key, default)` is the idiomatic one-liner.
ML pipelines raise custom exceptions throughout: `ConvergenceError` when gradient descent diverges; a wrapped `DataError` when reading a corrupt batch; PyTorch's `torch.cuda.OutOfMemoryError` when a batch is too large for GPU memory; scikit-learn's `NotFittedError` when `.predict()` is called before `.fit()`. The `try/finally` pattern also guards GPU memory: `try: train_step()` followed by `finally: torch.cuda.empty_cache()` ensures the cache is cleared even if the step raises.
If you remove it: Without exception handling, a single corrupt file or NaN loss in a 72-hour training run crashes the entire job. Exception handling is the difference between a fragile script and a production-grade pipeline that logs errors, skips bad batches, and keeps running.