|$ curl https://forge-ai.dev/api/markdown?path=docs/python/errors
$cat docs/python-—-error-handling.md
updated Recently·15 min read·published
Python — Error Handling
◆Python◆Intermediate
Introduction
Python uses exceptions to signal and handle errors. The try-except-else-finally pattern gives you fine-grained control over error recovery and resource cleanup.
Exception Hierarchy
All exceptions inherit from BaseException. You should only catch Exception or its subclasses, not BaseException directly.
hierarchy.py
Python
| 1 | # Exception hierarchy (simplified): |
| 2 | # BaseException |
| 3 | # ├── SystemExit |
| 4 | # ├── KeyboardInterrupt |
| 5 | # ├── GeneratorExit |
| 6 | # └── Exception |
| 7 | # ├── StopIteration |
| 8 | # ├── ArithmeticError |
| 9 | # │ ├── ZeroDivisionError |
| 10 | # │ └── OverflowError |
| 11 | # ├── LookupError |
| 12 | # │ ├── IndexError |
| 13 | # │ └── KeyError |
| 14 | # ├── ValueError |
| 15 | # ├── TypeError |
| 16 | # ├── RuntimeError |
| 17 | # ├── FileNotFoundError (OSError) |
| 18 | # ├── AttributeError |
| 19 | # ├── ImportError |
| 20 | # │ └── ModuleNotFoundError |
| 21 | # └── AssertionError |
| 22 | |
| 23 | # Common built-in exceptions: |
| 24 | # - ValueError: argument has right type but wrong value |
| 25 | # - TypeError: operation applied to wrong type |
| 26 | # - IndexError: sequence index out of range |
| 27 | # - KeyError: dict key not found |
| 28 | # - FileNotFoundError: file doesn't exist |
| 29 | # - AttributeError: object has no attribute |
| 30 | # - ImportError: module not found |
| 31 | # - RuntimeError: generic error with no better category |
Try / Except / Else / Finally
try_except.py
Python
| 1 | # Basic try-except |
| 2 | try: |
| 3 | x = int("not a number") |
| 4 | except ValueError: |
| 5 | print("conversion failed") |
| 6 | |
| 7 | # Multiple exception types |
| 8 | try: |
| 9 | result = 100 / int(input("enter: ")) |
| 10 | except ValueError: |
| 11 | print("not a number") |
| 12 | except ZeroDivisionError: |
| 13 | print("can't divide by zero") |
| 14 | |
| 15 | # Catch multiple in one except |
| 16 | try: |
| 17 | risky_operation() |
| 18 | except (ValueError, TypeError, RuntimeError) as e: |
| 19 | print(f"caught: {e}") |
| 20 | |
| 21 | # Bare except — catches everything (DANGEROUS) |
| 22 | try: |
| 23 | risky() |
| 24 | except: # also catches KeyboardInterrupt, SystemExit! |
| 25 | print("something went wrong") |
| 26 | |
| 27 | # Better: catch Exception only |
| 28 | try: |
| 29 | risky() |
| 30 | except Exception as e: |
| 31 | print(f"error: {e}") |
| 32 | |
| 33 | # Else — runs if NO exception was raised |
| 34 | try: |
| 35 | file = open("data.txt") |
| 36 | data = file.read() |
| 37 | except FileNotFoundError: |
| 38 | print("file not found") |
| 39 | else: |
| 40 | print(f"read {len(data)} characters") # only if no error |
| 41 | finally: |
| 42 | file.close() # ALWAYS runs — cleanup guaranteed |
| 43 | |
| 44 | # Finally without except — run cleanup even on error |
| 45 | try: |
| 46 | database.query("DELETE FROM users") |
| 47 | finally: |
| 48 | database.close() # runs even if query raises |
✓
best practice
Be as specific as possible with your exception types. A bare except: silently catches KeyboardInterrupt and SystemExit, making your program impossible to kill. Always use except Exception: at minimum.
Raising Exceptions
raising.py
Python
| 1 | # Raise with message |
| 2 | raise ValueError("invalid input") |
| 3 | |
| 4 | # Re-raise (preserve original traceback) |
| 5 | try: |
| 6 | risky() |
| 7 | except ValueError: |
| 8 | print("logging...") |
| 9 | raise # re-raises the same exception |
| 10 | |
| 11 | # Raise from — chain exceptions |
| 12 | try: |
| 13 | int("not a number") |
| 14 | except ValueError as e: |
| 15 | raise RuntimeError("parsing failed") from e |
| 16 | # Traceback shows: "The above exception was the direct cause..." |
| 17 | |
| 18 | # Suppress chaining with from None |
| 19 | try: |
| 20 | int("not a number") |
| 21 | except ValueError as e: |
| 22 | raise RuntimeError("parsing failed") from None |
| 23 | # Hides the original ValueError from traceback |
| 24 | |
| 25 | # Assertions — for debugging, not error handling |
| 26 | def divide(a, b): |
| 27 | assert b != 0, "division by zero" |
| 28 | return a / b |
| 29 | |
| 30 | # Assertions can be disabled with -O flag: |
| 31 | # python -O script.py |
Custom Exceptions
custom.py
Python
| 1 | # Custom exception — just subclass Exception |
| 2 | class ValidationError(Exception): |
| 3 | """Raised when data validation fails.""" |
| 4 | pass |
| 5 | |
| 6 | class InsufficientFundsError(Exception): |
| 7 | def __init__(self, balance: float, amount: float): |
| 8 | self.balance = balance |
| 9 | self.amount = amount |
| 10 | super().__init__(f"need {amount}, have {balance}") |
| 11 | |
| 12 | # Usage |
| 13 | def withdraw(balance: float, amount: float): |
| 14 | if amount > balance: |
| 15 | raise InsufficientFundsError(balance, amount) |
| 16 | return balance - amount |
| 17 | |
| 18 | try: |
| 19 | withdraw(100, 200) |
| 20 | except InsufficientFundsError as e: |
| 21 | print(f"short by ${e.amount - e.balance:.2f}") |
| 22 | |
| 23 | # Best practices for custom exceptions: |
| 24 | # 1. Inherit from Exception (not BaseException) |
| 25 | # 2. Name ends with "Error" |
| 26 | # 3. Keep them thin — just pass is often enough |
| 27 | # 4. Add attributes for context if needed |
| 28 | # 5. Module-level: define in exceptions.py module |
Context Managers
The with statement uses context managers for resource management. They guarantee setup and teardown, even in the face of exceptions.
context_managers.py
Python
| 1 | # File as context manager |
| 2 | with open("file.txt", "r") as f: |
| 3 | content = f.read() |
| 4 | # File automatically closed, even if exception in block |
| 5 | |
| 6 | # Multiple context managers |
| 7 | with open("input.txt") as infile, open("output.txt", "w") as outfile: |
| 8 | outfile.write(infile.read()) |
| 9 | |
| 10 | # Database connection |
| 11 | with db_connection() as conn: |
| 12 | conn.execute("SELECT ...") |
| 13 | # Auto-commits or rolls back |
| 14 | |
| 15 | # Creating context managers — class-based |
| 16 | class ManagedFile: |
| 17 | def __init__(self, filename: str, mode: str = "r"): |
| 18 | self.filename = filename |
| 19 | self.mode = mode |
| 20 | |
| 21 | def __enter__(self): |
| 22 | self.file = open(self.filename, self.mode) |
| 23 | return self.file # bound to |
ℹ
info
Prefer the @contextmanager decorator over writing a class when you just need simple setup/teardown. It's less code and harder to get wrong.
Error Handling Best Practices
best_practices.py
Python
| 1 | # 1. EAFP vs LBYL |
| 2 | # Python prefers EAFP: "Easier to Ask Forgiveness than Permission" |
| 3 | |
| 4 | # LBYL (Look Before You Leap) — non-Pythonic |
| 5 | if os.path.exists("file.txt"): |
| 6 | with open("file.txt") as f: |
| 7 | data = f.read() |
| 8 | |
| 9 | # EAFP (Easier to Ask Forgiveness) — Pythonic |
| 10 | try: |
| 11 | with open("file.txt") as f: |
| 12 | data = f.read() |
| 13 | except FileNotFoundError: |
| 14 | data = "" |
| 15 | |
| 16 | # 2. Don't swallow errors silently |
| 17 | try: |
| 18 | result = risky() |
| 19 | except Exception: |
| 20 | pass # BAD — hides all errors |
| 21 | |
| 22 | # 3. Be specific about what you catch |
| 23 | try: |
| 24 | result = int(value) |
| 25 | except ValueError: # GOOD — specific |
| 26 | result = 0 |
| 27 | |
| 28 | # 4. Keep try blocks minimal |
| 29 | # BAD |
| 30 | try: |
| 31 | user = get_user() |
| 32 | validated = validate(user) |
| 33 | result = save(validated) |
| 34 | except Exception as e: |
| 35 | print(e) |
| 36 | |
| 37 | # GOOD — each risky operation wrapped individually |
| 38 | try: |
| 39 | user = get_user() |
| 40 | except UserNotFoundError: |
| 41 | return fallback_user() |
| 42 | |
| 43 | try: |
| 44 | save(validate(user)) |
| 45 | except ValidationError as e: |
| 46 | return {"error": str(e)} |
| 47 | |
| 48 | # 5. Use logging, not print |
| 49 | import logging |
| 50 | logger = logging.getLogger(__name__) |
| 51 | |
| 52 | try: |
| 53 | risky() |
| 54 | except Exception: |
| 55 | logger.exception("operation failed") # logs traceback |
$Blueprint — Engineering Documentation·Section ID: PYTHON-ERR·Revision: 1.0