Built-in errors like
ValueError and TypeError are useful, but they often hide the real problem. When your program fails because a user has no credit left, or an item is out of stock, you want a name that means something to you. That is where custom exceptions come in.Defining the minimum
class InsufficientFunds(Exception):
"""Raised when a transaction exceeds available balance."""
try:
raise InsufficientFunds("Not enough money")
except InsufficientFunds as e:
print(e)Why specific names beat generic catches
Catching
Exception for everything is like putting a band-aid on every cut. If your function can fail because the port is wrong, or because the file does not exist, you want two different names so callers know exactly what went wrong and how to react.Exception families
class ShopError(Exception):
"""Base for all shop-related problems."""
class OutOfStock(ShopError):
pass
def buy(item, qty=1):
if item == 'coffee':
raise OutOfStock("Coffee is gone")
return f'Purchased {qty} x {item}'
try:
buy('coffee')
except ShopError as e:
print(f"Shop problem: {e}")Because
OutOfStock inherits from ShopError, a single except ShopError catches both. This keeps your error handling tidy when you have many related failure modes.Chaining and notes
class ConfigError(Exception):
pass
def read_level(settings):
try:
return int(settings['level'])
except ValueError as err:
raise ConfigError('level must be a number') from err
except KeyError as err:
err.add_note('check the settings for a level entry')
raise
from and adding context notesraise ... from err attaches the original error as __cause__, so tracebacks show both. .add_note() (Python 3.11+) lets you add extra context without changing the exception type.