Skip to Content

Exceptions & Error Handling

When to use

Handle recoverable failures (network timeout, JSON parse, missing key) without crashing the program.

Analogy

try/except is a safety net under a tightrope; raise is shouting up the ladder; the ladder is the call stack.

Data-flow diagram

try:
    io.read() ---+
                  | raises OSError
                  v
except OSError as e:
    handle(e)
    raise DomainError('io failed') from e
finally:
    cleanup()        # ALWAYS runs

Deep explanation

Python’s exception model is EAFP - Easier to Ask Forgiveness than Permission. Use try: x[k] except KeyError: rather than if k in d: (which races on concurrent mutation and does two lookups). Catch specific exceptions (not bare except:) so you don’t swallow KeyboardInterrupt/SystemExit. Use raise X from Y (PEP 3134) to preserve the cause chain.

Examples

Example 1

try:
    with open('data.json') as f:
        cfg = json.load(f)
except FileNotFoundError as e:
    cfg = {}
    log.info('defaults', path=e.filename)

specific except; bare except: would also catch KeyboardInterrupt.

Example 2

try:
    result = db.execute(q)
except psycopg2.OperationalError as e:
    raise ServiceUnavailable('db down') from e

convert internal exceptions to API exceptions; from e preserves chain.

Example 3

import contextlib
with contextlib.suppress(FileNotFoundError):
    os.remove('cache.tmp')   # ok if gone

contextlib.suppress for ‘I know this raises and I’m fine with that’.

Common mistake

Swallowing with except: pass masks real bugs; bare except: catches KeyboardInterrupt/SystemExit too. Catching broad Exception and not re-raising leaves processes in inconsistent state.

Key takeaway

Specific exceptions; raise X from e at boundaries; never bare except/pass; finally for cleanup; with-statements for resources.

Production Failure Playbook

Failure scenario 1: swallowed-exception

Failure scenario 2: lost-exception-context