Python exceptions form a class hierarchy rooted at BaseException. Understanding this hierarchy is Essential for writing correct exception handlers.
│ │ ├── ConnectionRefusedError
│ │ ├── ConnectionResetError
│ │ ├── ConnectionAbortedError
│ ├── NotImplementedError
│ └── ModuleNotFoundError
### Catching by Hierarchy
value = int("not a number")
print(f"ValueError: {e}") # Catches ValueError specifically
print(f"LookupError: {e}") # Catches both KeyError and IndexError
print(f"Caught: {e}") # Catches all standard exceptions, not SystemExit/KeyboardInterrupt
Every library or application should define a custom base exception class:
class AppError(Exception):
"""Base exception for all application errors."""
def __init__(self, message, *, code=None, details=None):
super().__init__(message)
self.details = details or {}
return f"[{self.code}] {msg}"
class ConfigError(AppError):
"""Configuration-related errors."""
class NetworkError(AppError):
"""Network-related errors."""
class DatabaseError(AppError):
"""Database-related errors."""
class ValidationError(AppError):
"""Input validation errors."""
except FileNotFoundError as e:
raise ConfigError(f"Config file not found: {path}", code="CONFIG_NOT_FOUND") from e
raise ConfigError(f"Invalid config syntax in {path}", code="CONFIG_PARSE_ERROR") from e
raise ValueError("Unexpected token at line 42")
class ServerError(Exception):
def __init__(self, host, port, reason):
super().__init__(f"{host}:{port} — {reason}")
return f"ServerError({self.host!r}, {self.port!r}, {self.reason!r})"
e = ServerError("db.example.com", 5432, "connection refused")
print(str(e)) # db.example.com:5432 — connection refused
print(repr(e)) # ServerError("db.example.com', 5432, 'connection refused')
## EAFP vs LBYL
The Pythonic approach — try the operation and handle exceptions:
def get_value(data, key, default=None):
except (KeyError, TypeError, IndexError):
print(get_value({"a": 1}, "a")) # 1
print(get_value({"a": 1}, "b")) # None
print(get_value([1, 2, 3], 1)) # 2
print(get_value(42, "x")) # None
Check conditions before operating:
def get_value_lbyl(data, key, default=None):
if isinstance(data, dict) and key in data:
if isinstance(data, (list, tuple)) and isinstance(key, int) and 0 <= key < len(data):
print(get_value_lbyl({"a": 1}, "a")) # 1
print(get_value_lbyl({"a": 1}, "b")) # None
| Scenario | Prefer | Reason |
|---|
| File existence | EAFP (open + except) | TOCTOU race condition with LBYL |
| Dict key access | EAFP (try/except KeyError) | Cleaner, idiomatic |
| Type checking | LBYL (isinstance) | Wrong types are programmer errors |
| External API calls | EAFP + retry | Network conditions change |
| Configuration validation | LBYL at boundary | Fail fast, clear error messages |