BugForge
All lessons
Python · 04/10 6 min

Exceptions and context managers

Catch the right thing, keep the cause, roll back on failure, and always clean up.

Catch narrowly

try:
    user = users[user_id]
except:                    # ❌ also catches KeyboardInterrupt, SystemExit, typos (NameError)
    ...
except Exception:          # still broad: hides real bugs
    ...
except KeyError:           # ✅ the one failure you expect
    raise NotFound(user_id)

Order matters: the first matching except wins, so put specific classes before general ones.

Custom exceptions

class LedgerError(Exception): pass
class InsufficientFunds(LedgerError):
    def __init__(self, needed, available):
        super().__init__(f"need {needed}, have {available}")
        self.needed, self.available = needed, available

Tests usually check the type (assertRaises(InsufficientFunds)), sometimes the message.

Keep the cause

try:
    amount = Decimal(raw)
except InvalidOperation as e:
    raise ValueError(f"bad amount: {raw!r}") from e

else and finally

try:
    conn = connect()
except ConnectionError:
    log("down")
else:
    use(conn)          # runs only if no exception
finally:
    cleanup()          # always runs, even on return

Atomic operations

When an operation has several steps, validate everything first, then mutate. Or snapshot and restore on failure:

def transfer(src, dst, amount):
    if dst is None: raise ValueError("no destination")   # check before touching src
    src.withdraw(amount)
    dst.deposit(amount)

Context managers

with open(path) as f:          # closed even if the block raises
    data = f.read()

from contextlib import contextmanager
@contextmanager
def transaction(store):
    snapshot = dict(store)
    try:
        yield store
    except Exception:
        store.clear(); store.update(snapshot)   # roll back
        raise

dict[key] vs .get

d[key] raises KeyError; d.get(key) returns None. Use .get when missing is normal, [] when missing is a bug you want to see.

Next in Python
Classes and dataclasses