Error Handling and Context Managers¶
Python's exception handling uses try/except blocks and follows the EAFP (Easier to Ask Forgiveness than Permission) philosophy. Context managers (with statement) provide automatic resource cleanup. Custom exceptions create meaningful error hierarchies for applications.
Key Facts¶
- Never use bare
except:- it catchesSystemExit,KeyboardInterrupt finallyalways runs, even ifreturnis inside try/exceptelseblock runs only if no exception was raised intry- EAFP is preferred over LBYL (Look Before You Leap) in Python
- Context managers (
with) guarantee cleanup even on exceptions - Custom exceptions should inherit from
Exception, notBaseException
Patterns¶
Basic try/except¶
Multiple Exception Types¶
try:
value = int(input("Number: "))
result = 10 / value
except ValueError:
print("Not a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
except (TypeError, AttributeError) as e:
print(f"Error: {e}")
else and finally¶
try:
f = open("file.txt")
data = f.read()
except FileNotFoundError:
print("File not found")
else:
print(f"Read {len(data)} chars") # only if no exception
finally:
f.close() # ALWAYS runs
Raising Exceptions¶
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
# Re-raise current exception
try:
process()
except ValueError:
logging.error("Failed")
raise # re-raise same exception
# Exception chaining
try:
value = int(user_input)
except ValueError as e:
raise RuntimeError("Invalid config") from e
Custom Exceptions¶
class AppError(Exception):
"""Base exception for application."""
pass
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class InsufficientFundsError(AppError):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Cannot withdraw {amount}, balance is {balance}")
Context Managers (with statement)¶
# File handling (automatic close)
with open("file.txt") as f:
data = f.read()
# Multiple context managers
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
Custom Context Manager (class)¶
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.time() - self.start
print(f"Elapsed: {self.elapsed:.2f}s")
return False # don't suppress exceptions
with Timer() as t:
heavy_computation()
Custom Context Manager (contextlib)¶
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
yield # code inside 'with' block runs here
print(f"Elapsed: {time.time() - start:.2f}s")
EAFP vs LBYL¶
# EAFP (Pythonic) - try first, handle failure
try:
value = my_dict[key]
except KeyError:
value = default
# LBYL (less Pythonic) - check first
if key in my_dict:
value = my_dict[key]
else:
value = default
Suppress Specific Exceptions¶
Logging Exceptions¶
import logging
try:
process()
except Exception:
logging.exception("Processing failed") # logs full traceback
raise
Exception Hierarchy (key classes)¶
BaseException
+-- SystemExit, KeyboardInterrupt, GeneratorExit
+-- Exception
+-- ArithmeticError (ZeroDivisionError, OverflowError)
+-- AttributeError
+-- ImportError (ModuleNotFoundError)
+-- LookupError (IndexError, KeyError)
+-- NameError (UnboundLocalError)
+-- OSError (FileNotFoundError, PermissionError)
+-- TypeError, ValueError, RuntimeError
Gotchas¶
except Exception as eis acceptable; bareexcept:catches too much- Execution order: try -> except (if error) -> else (if no error) -> finally (always)
finallyruns even withreturnin try/except - thefinallyreturn value wins__exit__returningTruesuppresses the exception - use carefully- Retry loops should have a maximum attempt count to avoid infinite loops
See Also¶
- functions - early return patterns
- fastapi fundamentals - HTTP exception handling
- testing with pytest - testing exception raising