Error Handling, Logging & Testing with pytest
Why Error Handling Matters
Amateur code crashes. Professional code fails gracefully.
Every program encounters unexpected conditions: a file doesn't exist, a network times out, a user passes bad data. How your code responds to those conditions is the difference between software people trust and software people avoid.
Python's exception system is one of the most expressive in any language. Understanding it deeply — not just the mechanics, but the design — will change how you write every function.
The Exception Hierarchy
Python's exceptions form an inheritance tree rooted at BaseException. Understanding the hierarchy explains which except clauses catch what:
The key insight: except Exception catches everything except SystemExit, KeyboardInterrupt, and GeneratorExit. Those three inherit directly from BaseException because they represent program lifecycle events, not errors in your logic. Catching them accidentally is a bug — your program would ignore Ctrl+C, for example.
try / except / else / finally
Each block has a specific purpose — using them correctly communicates intent:
The else block is underused but important. Code in else runs only when try succeeded, but is outside the try block — so if else itself raises an exception, it won't be caught by the except clauses above. This makes the intent clear: the except handlers are for risky_operation() specifically, not for everything that follows.
Catching Specific vs Broad Exceptions
The cardinal rule: catch the most specific exception you can handle.
When is except Exception acceptable? At the outermost layer of a system — a web server's request handler, a background job runner — where you truly want to catch and log any crash without killing the process. Even then, log the full traceback.
raise, raise from, and Re-raising
raise creates a new exception. raise from chains exceptions, preserving the original context. bare raise re-raises the current exception.
Custom Exception Classes
Custom exceptions are first-class objects. Give them structured data, not just strings:
Context Managers
Context managers guarantee cleanup — they run __exit__ even if an exception occurs inside the with block. This is more reliable than try/finally because it packages the cleanup with the resource.
The logging Module
print() is for development. logging is for production. The difference: log records have levels, timestamps, caller info, and can be routed to multiple destinations (console, file, external service) simultaneously.
pytest: Testing Your Code
pytest is Python's most popular testing framework. Its philosophy: test functions are just functions, assertions use Python's built-in assert statement, and failures give you rich context.
The core patterns:
PROJECT: Validated Data Pipeline
We'll build a complete data processing pipeline with:
- Three custom exception classes with structured context
- A
DataValidatorthat enforces schema, types, and value ranges - A
DataProcessorthat reads CSV data, validates each row, transforms, and reports errors - A full test suite demonstrating every failure mode
Now let's see the full test suite:
Key Takeaways
- Exception hierarchy matters: catch
Exceptiononly at system boundaries; prefer specific exceptions that communicate intent except Exceptionhides bugs: a broad catch that logs "something went wrong" obscuresAttributeError,NameError, and other programmer mistakeselseclarifies intent: code that runs only on success belongs inelse, not insidetry— it won't be caught by yourexceptclausesraise frompreserves cause: useraise NewError("...") from original_errorso callers see the full chain; useraise ... from Noneto suppress implementation details- Custom exceptions carry data: give exceptions structured fields (
code,field,row_num) instead of cramming everything into the message string - Context managers guarantee cleanup:
__exit__is called even when exceptions occur — more reliable thantry/finallyfor resource management - Logging beats printing: use named loggers (
logging.getLogger(__name__)), appropriate levels, andextra=for structured context; print is for REPL exploration - pytest is just functions and assert: test functions start with
test_, use plainassert, andpytest.raisesfor exception testing — parametrize eliminates copy-paste test boilerplate