try:
result = risky_operation()
except ValueError as e:
print(f"Bad value: {e}")
except (TypeError, KeyError) as e:
print(f"Type or key issue: {e}")
else:
print("Ran only if no exception was raised")
finally:
print("Always runs — cleanup goes here")
Order matters: except clauses are checked top to bottom, and the first matching one wins — a broad except Exception placed before a specific except ValueError would swallow the ValueError before the more specific handler ever saw it, so specific exceptions belong before general ones.
except: is almost always wrongtry:
do_something()
except: # catches EVERYTHING, including KeyboardInterrupt and SystemExit
pass
A bare except: (or except BaseException:) catches signals meant to actually stop the program — KeyboardInterrupt (Ctrl+C) and SystemExit — silently swallowing them along with real bugs. except Exception: is the correct "catch almost anything" clause, since Exception deliberately excludes those two.
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
super().__init__(f"Cannot withdraw {amount}, balance is {balance}")
self.balance = balance
self.amount = amount
try:
withdraw(account, 500)
except InsufficientFundsError as e:
log.error("withdrawal failed")
raise # re-raises the SAME exception with its original traceback intact
raise with no argument inside an except block re-raises the exception currently being handled, preserving its original traceback — raise e instead loses some of that context (it looks like the exception originated at this raise line, not its actual source). Custom exception classes that carry structured data (self.balance, self.amount above) let calling code inspect why something failed programmatically instead of parsing an error string.