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.
Practise it now
Next in Python
Classes and dataclasses