Errors and exceptions

Catch what you can handle, let the rest travel, and raise errors that tell the next person what to do.

11of 24
≈ 13Min read
5Parts

The four clauses

else holds the code that should run only on success, and keeping it out of the try means you are not accidentally catching exceptions from it. finally runs whatever happens, which is what makes it the right place for cleanup — though a with block is usually better still.

py
try:
    data = json.loads(text)
except json.JSONDecodeError as err:
    log(f"bad payload: {err}")
    data = {}
else:
    log("parsed cleanly")        # only if try raised nothing
finally:
    close(handle)                # always — return, raise, break, everything

Catch narrowly

A bare except: catches KeyboardInterrupt and SystemExit, so it can stop a program being stopped. except Exception: is the widest you should ever write, and only where you genuinely intend to keep going.

The real rule: catch the exception you know how to handle, at the level where you know what to do about it. An exception you cannot act on should travel up to somewhere that can.

py
except Exception:          # too wide, but at least it lets KeyboardInterrupt through
except (ValueError, TypeError) as err:    # a tuple, when several are equivalent
except:                    # NEVER — this catches SystemExit and Ctrl-C too

The hierarchy, briefly

LookupError covers KeyError and IndexError; ArithmeticError covers ZeroDivisionError and OverflowError. Catching the parent is how you say "any of that family".

ExceptionMeans
ValueErrorThe right type, the wrong value — int("abc")
TypeErrorThe wrong type — "3" + 4
KeyError / IndexErrorNot in the dict / past the end of the sequence
AttributeErrorNo such attribute — often None where an object was expected
FileNotFoundErrorA subclass of OSError, like most I/O problems
StopIterationAn iterator is finished — the protocol, not a failure

Raising well

A good message says what was expected, what arrived, and — if you can — what to do. Define your own exception class when callers might want to catch your failure specifically; inherit from Exception, never from BaseException.

raise ... from err preserves the chain, so the traceback shows both. A bare raise inside an except re-raises the current exception untouched, which is how you log and then let it continue.

py
if quantity < 0:
    raise ValueError(f"quantity must be at least 0, got {quantity}")

class CartEmpty(Exception):
    """Raised when checkout is attempted with nothing in the cart."""

try:
    fetch(url)
except TimeoutError as err:
    raise ServiceUnavailable(url) from err    # keeps the original in __cause__

Ask forgiveness, not permission

EAFP is the Python house style, and not only for taste: the check-then-act version does the work twice and can be wrong by the time it acts. try costs almost nothing when nothing is raised.

The exception is a loop where failure is the common case — raising is not free, so a dict.get beats an exception you expect to hit a million times.

py
# LBYL — look before you leap: two lookups, and a race in between
if key in data:
    use(data[key])

# EAFP — easier to ask forgiveness: one lookup, and no race
try:
    use(data[key])
except KeyError:
    default()

Practice