Error Handling#
Production-quality Python distinguishes expected outcomes from exceptional failures, designs exception hierarchies deliberately, and cleans up resources reliably. This page goes beyond Basic Error Handling with chaining, context utilities, and patterns for robust code.
How to use this page
Know when to raise vs return. Use with File Handling for I/O errors and Functions and Scope for contract design.
At a glance
| Track | Python Intermediate |
| Sections | 10 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Exception hierarchy (detailed) · Designing exception types · try / except / else / finally — full semantics · Exception chaining · Exception groups (3.11+) · EAFP vs LBYL — intermediate guidance · contextlib utilities · Logging vs raising · … (+2 more)
- Exception hierarchy (detailed)
- Designing exception types
try/except/else/finally— full semantics- Exception chaining
- Exception groups (3.11+)
- EAFP vs LBYL — intermediate guidance
contextlibutilities- Logging vs raising
- Assertions vs exceptions
- Interview vs production patterns
Exception hierarchy (detailed)#
BaseException
├── SystemExit # sys.exit()
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit
└── Exception # Catch this, not BaseException
├── StopIteration
├── StopAsyncIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError # I/O, OS failures
│ ├── FileNotFoundError
│ ├── PermissionError
│ ├── TimeoutError
│ └── ...
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ └── RecursionError
├── SyntaxError
├── SystemError
├── TypeError
└── ValueError
Catch Exception, never bare BaseException — you do not want to catch KeyboardInterrupt or SystemExit accidentally.
try:
risky()
except Exception as e:
log.exception("failed") # includes traceback
raise # re-raise after logging
Designing exception types#
Simple domain exception#
class AppError(Exception):
"""Base for all application errors."""
class ValidationError(AppError):
"""User input failed validation."""
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
self.resource = resource
self.id = id
super().__init__(f"{resource} {id!r} not found")
Guidelines:
- Inherit from
Exception(or your app base). - Put structured data on the exception object (
resource,id). - Keep messages actionable for humans.
- Don't over-granularize — 5–10 types per project is often enough.
When to subclass ValueError vs Exception#
| Inherit from | When |
|---|---|
ValueError |
Bad value for otherwise valid API (like built-ins) |
AppError |
Domain-specific failures |
RuntimeError |
Internal inconsistency / bug-like state |
try / except / else / finally — full semantics#
def process_file(path: str) -> dict:
f = None
try:
f = open(path)
data = parse(f.read())
except FileNotFoundError:
return {}
except UnicodeDecodeError as e:
raise ValueError(f"not valid UTF-8: {path}") from e
else:
audit.log(f"processed {path}")
finally:
if f is not None:
f.close()
return data
| Clause | Runs when |
|---|---|
try |
Always first |
except |
Matching exception raised in try |
else |
try completed without exception |
finally |
Always — before return/break propagates |
finally and return#
If finally also executes return, it overrides the try/except return (avoid this).
Exception in except or finally#
If except raises a new exception, original is lost unless chained. If finally raises, it may mask prior exception — keep finally minimal (cleanup only).
Exception chaining#
try:
config = json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigurationError("invalid JSON config") from e
| Form | Effect |
|---|---|
raise X from e |
X.__cause__ = e — show both in traceback |
raise X from None |
Suppress context display |
bare raise |
Re-raise current exception with original traceback |
Use chaining when wrapping low-level errors (I/O, parse) into domain errors.
Exception groups (3.11+)#
Parallel failures in structured concurrency:
try:
raise ExceptionGroup("failures", [
ValueError("bad value"),
TypeError("bad type"),
])
except* ValueError as eg:
handle_value_errors(eg.exceptions)
except* TypeError as eg:
handle_type_errors(eg.exceptions)
Relevant for asyncio.TaskGroup and batch validation — less common in DSA interviews.
EAFP vs LBYL — intermediate guidance#
| Style | Pythonic when | Example |
|---|---|---|
| EAFP | Happy path common; failure exceptional | try: d[k] / except KeyError |
| LBYL | Failure common; check cheap | if key in d |
.get() |
Missing key with default | d.get(k, 0) |
# EAFP — file exists and readable
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
return default_content
# LBYL — avoid exception in tight loop with frequent misses
if key in cache:
return cache[key]
For dict counting, .get() is clearest. For "key must exist or crash", use d[k] directly.
contextlib utilities#
suppress — ignore specific exceptions#
Use sparingly — silent failure hides bugs.
contextmanager — write your own with#
from contextlib import contextmanager
@contextmanager
def temporary_setting(name, value):
old = getattr(config, name)
setattr(config, name, value)
try:
yield
finally:
setattr(config, name, old)
ExitStack — dynamic number of context managers#
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
process_all(files)
# all files closed
Logging vs raising#
| Situation | Action |
|---|---|
| Caller can recover | Raise specific exception |
| Cannot continue | Raise — don't return error codes |
| Record and continue | Log warning, handle locally |
| Unexpected bug | Log exception (exc_info=True), re-raise or crash |
import logging
log = logging.getLogger(__name__)
try:
result = compute()
except ValueError as e:
log.warning("invalid input: %s", e)
return fallback
except Exception:
log.exception("unexpected failure")
raise
Never except Exception: pass in production.
Assertions vs exceptions#
| Use | Tool |
|---|---|
| Internal invariants / debug | assert |
| User input validation | raise ValueError |
| API contract violations | Custom exception |
Interview vs production patterns#
Algorithm problems — prefer sentinels#
def find(nums: list[int], target: int) -> int:
for i, x in enumerate(nums):
if x == target:
return i
return -1
API / parsing — raise on bad input#
def parse_port(s: str) -> int:
try:
port = int(s)
except ValueError as e:
raise ValueError(f"invalid port: {s!r}") from e
if not 1 <= port <= 65535:
raise ValueError(f"port out of range: {port}")
return port
Multiple error collection#
errors: list[str] = []
if not name:
errors.append("name required")
if age < 0:
errors.append("age must be non-negative")
if errors:
raise ValidationError("; ".join(errors))
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
Bare except: |
Catches KeyboardInterrupt | Catch Exception or specific |
except Exception: pass |
Silent failure | Log and handle |
| Assert for user input | Disabled with -O |
Raise ValueError |
| Broad catch then re-wrap poorly | Lost context | raise ... from e |
Heavy logic in finally |
Masks exceptions | Cleanup only |
| Using exceptions for flow control in hot loops | Slow | .get(), membership test |
Mental model checklist#
- What is the difference between
elseandfinallyin try/except? - When should you use
raise ... from e? - Why not catch
BaseException? - When is EAFP better than LBYL for your case?
- What happens if
finallyraises an exception?
What's next#
| Topic | Page |
|---|---|
| Basic exceptions | Basic Error Handling |
| File I/O errors | File Handling |
| Unit testing errors | Unit Testing |
| Logging | Data Handling and Standard Library |