Frontend Labs
⋯

7. Errors and exceptions

7.1 try, except, else, finally

Python reports errors by raising exceptions. An unhandled exception unwinds the stack, prints a traceback, and exits with status 1.

def read_port(text: str) -> int:
    try:
        port = int(text)
    except ValueError:
        raise ValueError(f"not a number: {text!r}") from None
    else:                          # runs only if no exception
        if not 0 < port < 65536:
            raise ValueError(f"out of range: {port}")
        return port
    finally:                       # always runs: cleanup goes here
        pass

for t in ["443", "http", "70000"]:
    try:
        print(read_port(t))
    except ValueError as e:
        print("error:", e)

Reading a traceback: read it bottom-up. The last line is the exception type and message; the lines above are the call stack, innermost call last. Since 3.11, tracebacks point at the exact sub-expression with ^^^^ markers, and since 3.13 they are coloured. Appendix A decodes the 15 exceptions you will see most.

7.2 The rules for handling exceptions

  1. Catch the narrowest exception you can handle. except ValueError, not except Exception.
  2. Never write a bare except:. It also catches KeyboardInterrupt and SystemExit, so Ctrl-C stops working.
  3. Do not swallow errors. except Exception: pass turns a crash into silently wrong data. If you catch broadly at a boundary (a request handler, a worker loop), log with the traceback (logger.exception).
  4. Chain with raise ... from err when you translate an exception, so the original cause stays in the traceback.
  5. EAFP over LBYL. "Easier to ask forgiveness than permission": try the operation and handle failure, rather than checking first. Checking first is often a race, as the OS guide Ch 17 shows for files:
import os

# LBYL: racy, the file can vanish between the check and the open (TOCTOU)
if os.path.exists("config.toml"):
    pass  # open(...) might still fail here

# EAFP: one operation, one place to handle failure
try:
    with open("config.toml", "rb") as f:
        raw = f.read()
except FileNotFoundError:
    raw = b""

7.3 The exception hierarchy

BaseException
 ├── SystemExit, KeyboardInterrupt, GeneratorExit   <- not errors; don't catch these by accident
 └── Exception                                       <- catch-all for real errors
      ├── ValueError, TypeError, KeyError, IndexError, AttributeError
      ├── ArithmeticError → ZeroDivisionError, OverflowError
      ├── OSError → FileNotFoundError, PermissionError, ConnectionError, TimeoutError, ...
      ├── RuntimeError → RecursionError, NotImplementedError
      └── ExceptionGroup (3.11+)

OSError carries errno, the same error number the kernel returned (OS Ch 2):

import errno
try:
    open("/root/secret")
except OSError as e:
    print(e.errno == errno.EACCES or e.errno == errno.ENOENT, e.strerror)

7.4 Custom exceptions

Give your library one base exception, and specific subclasses under it:

class FetchError(Exception):
    """Base class for errors from this module."""

class RateLimited(FetchError):
    def __init__(self, retry_after: float) -> None:
        super().__init__(f"rate limited; retry after {retry_after}s")
        self.retry_after = retry_after

try:
    raise RateLimited(2.5)
except FetchError as e:              # callers can catch the whole family
    print(type(e).__name__, e, getattr(e, "retry_after", None))

requests follows exactly this design (RequestException and its subclasses); Chapter 65 reads it.

7.5 Exception groups and add_note

Concurrent code can fail in several places at once. 3.11 added ExceptionGroup and except* to handle that (asyncio's TaskGroup, Ch 29, raises them), and add_note to attach context:

def validate(rows: list[dict]) -> None:
    errors = []
    for i, row in enumerate(rows):
        if "id" not in row:
            e = KeyError("id")
            e.add_note(f"row {i}: {row}")
            errors.append(e)
    if errors:
        raise ExceptionGroup("validation failed", errors)

try:
    validate([{"id": 1}, {}, {"name": "x"}])
except* KeyError as eg:
    for e in eg.exceptions:
        print("missing", e, "|", e.__notes__[0])

Generated from real-life-python-guide.md · 8 Oct 2026 16:36