|$ 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

PythonIntermediate
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
2try:
3 x = int("not a number")
4except ValueError:
5 print("conversion failed")
6
7# Multiple exception types
8try:
9 result = 100 / int(input("enter: "))
10except ValueError:
11 print("not a number")
12except ZeroDivisionError:
13 print("can't divide by zero")
14
15# Catch multiple in one except
16try:
17 risky_operation()
18except (ValueError, TypeError, RuntimeError) as e:
19 print(f"caught: {e}")
20
21# Bare except — catches everything (DANGEROUS)
22try:
23 risky()
24except: # also catches KeyboardInterrupt, SystemExit!
25 print("something went wrong")
26
27# Better: catch Exception only
28try:
29 risky()
30except Exception as e:
31 print(f"error: {e}")
32
33# Else — runs if NO exception was raised
34try:
35 file = open("data.txt")
36 data = file.read()
37except FileNotFoundError:
38 print("file not found")
39else:
40 print(f"read {len(data)} characters") # only if no error
41finally:
42 file.close() # ALWAYS runs — cleanup guaranteed
43
44# Finally without except — run cleanup even on error
45try:
46 database.query("DELETE FROM users")
47finally:
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
2raise ValueError("invalid input")
3
4# Re-raise (preserve original traceback)
5try:
6 risky()
7except ValueError:
8 print("logging...")
9 raise # re-raises the same exception
10
11# Raise from — chain exceptions
12try:
13 int("not a number")
14except 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
19try:
20 int("not a number")
21except 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
26def 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
2class ValidationError(Exception):
3 """Raised when data validation fails."""
4 pass
5
6class 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
13def withdraw(balance: float, amount: float):
14 if amount > balance:
15 raise InsufficientFundsError(balance, amount)
16 return balance - amount
17
18try:
19 withdraw(100, 200)
20except 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
2with open("file.txt", "r") as f:
3 content = f.read()
4# File automatically closed, even if exception in block
5
6# Multiple context managers
7with open("input.txt") as infile, open("output.txt", "w") as outfile:
8 outfile.write(infile.read())
9
10# Database connection
11with db_connection() as conn:
12 conn.execute("SELECT ...")
13# Auto-commits or rolls back
14
15# Creating context managers — class-based
16class 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
5if os.path.exists("file.txt"):
6 with open("file.txt") as f:
7 data = f.read()
8
9# EAFP (Easier to Ask Forgiveness) — Pythonic
10try:
11 with open("file.txt") as f:
12 data = f.read()
13except FileNotFoundError:
14 data = ""
15
16# 2. Don't swallow errors silently
17try:
18 result = risky()
19except Exception:
20 pass # BAD — hides all errors
21
22# 3. Be specific about what you catch
23try:
24 result = int(value)
25except ValueError: # GOOD — specific
26 result = 0
27
28# 4. Keep try blocks minimal
29# BAD
30try:
31 user = get_user()
32 validated = validate(user)
33 result = save(validated)
34except Exception as e:
35 print(e)
36
37# GOOD — each risky operation wrapped individually
38try:
39 user = get_user()
40except UserNotFoundError:
41 return fallback_user()
42
43try:
44 save(validate(user))
45except ValidationError as e:
46 return {"error": str(e)}
47
48# 5. Use logging, not print
49import logging
50logger = logging.getLogger(__name__)
51
52try:
53 risky()
54except Exception:
55 logger.exception("operation failed") # logs traceback
$Blueprint — Engineering Documentation·Section ID: PYTHON-ERR·Revision: 1.0