Errors#

Python signals failure with exceptions. Every error you’ve seen (ValueError, KeyError, FileNotFoundError) is a class that inherits from BaseException. You raise them, you catch them, you let them propagate.

Catching#

The full structure.

try:
    risky()
except (ValueError, KeyError) as e:
    handle(e)
except OSError:
    give_up()
else:
    # ran only if no exception was raised
    commit()
finally:
    # always runs (cleanup)
    close()

Catch what you actually expect. Catching Exception (or worse, BaseException) hides bugs.

EAFP, “Easier to Ask Forgiveness than Permission”, is the standard Python style.

# idiomatic
try:
    value = d[key]
except KeyError:
    value = default

# or:
value = d.get(key, default)

Raising#

raise ValueError(f"bad input: {x!r}")
raise RuntimeError("unreachable")        # belt-and-suspenders

# re-raise after logging
try:
    work()
except OSError:
    log.exception("work failed")
    raise

# chain with `from` to keep the original
try:
    parse(buf)
except ValueError as e:
    raise ConfigError("bad config") from e

Exception tree#

The hierarchy you’ll see most.

        flowchart TD
  B[BaseException]
  B --> SE[SystemExit]
  B --> KI[KeyboardInterrupt]
  B --> GE[GeneratorExit]
  B --> E["Exception<br/><i>catch this, not BaseException</i>"]
  E --> AE["ArithmeticError<br/>(ZeroDivisionError, OverflowError, ...)"]
  E --> LE["LookupError<br/>(IndexError, KeyError)"]
  E --> OS["OSError<br/>(FileNotFoundError, PermissionError,<br/>IsADirectoryError, ConnectionError, ...)"]
  E --> VE["ValueError<br/>(UnicodeDecodeError, ...)"]
  E --> TE[TypeError]
  E --> RE["RuntimeError<br/>(RecursionError, NotImplementedError)"]
  E --> SI["StopIteration / StopAsyncIteration"]
    

BaseException includes KeyboardInterrupt and SystemExit, which you almost never want to suppress.

Custom exceptions#

class ConfigError(Exception):
    """Raised when config can't be loaded or validated."""

class HTTPError(Exception):
    def __init__(self, status: int, body: str):
        super().__init__(f"HTTP {status}: {body[:80]}")
        self.status = status
        self.body = body

Inherit from a meaningful base. Avoid Exception directly when a more specific built-in fits (ValueError for bad input, LookupError for missing keys, etc.).

ExceptionGroup#

Since Python 3.11 you can raise / catch multiple exceptions together (e.g. parallel asyncio.gather):

try:
    await asyncio.gather(a(), b(), c())
except* ValueError as eg:
    for e in eg.exceptions:
        handle(e)

Assertions#

assert is for invariants the developer expects to hold, not for input validation; it’s stripped under python -O.

assert isinstance(x, int), "callers must pass ints"

Pitfalls#

  • Bare ``except:`` swallows KeyboardInterrupt / SystemExit. Always name the exception class.

  • ``except Exception as e: pass`` silently hides bugs. Log it or re-raise.

  • Exception in ``finally:`` can mask the original; use try / except / finally carefully.

  • Logging then re-raising, use log.exception(...), which attaches the current traceback.

See also: man 1 python, https://docs.python.org/3/library/exceptions.html.