Python Error Handling with try/except, finally, and Logging
I built robust error handling with targeted except clauses, custom exception types, and structured logging instead of bare prints. Scripts now fail safely, release resources in finally, and record every fault with context for debugging.
Objective & Context
Unhandled exceptions crash automation mid-task and leave systems inconsistent. This lab replaces broad except: blocks with specific handling, adds custom exceptions for domain errors, and routes faults through the logging module, the resilience layer behind the IR automation work.
Learning Objectives
- Catch specific exceptions instead of bare except clauses.
- Define domain exception types that carry context.
- Configure the logging module with levels and formats.
- Use finally and context managers for guaranteed cleanup.
- Decide what to handle, what to log, and what to let propagate.
Environment & Prerequisites
Prerequisites
Python 3.11with the standard libraryloggingmodule- A script that performs I/O or network calls (real failure modes to handle)
- A log destination: stderr or a file
- Assumed skill: functions and reading tracebacks
Step-by-Step Execution
1. Targeted handling with logging
import logging
log = logging.getLogger(__name__)
def fetch(): # placeholder: replace with a real call
raise TimeoutError("timed out")
def cleanup(): # placeholder: release resources here
pass
try:
data = fetch()
except (TimeoutError, ConnectionError) as e:
log.error("fetch failed: %s", e)
raise
finally:
cleanup()
Expected output:
2026-06-17 09:14:02 ERROR fetch failed: timed out
What this teaches: Catching the two expected network exceptions keeps the handler honest; anything unexpected still propagates with a full traceback instead of being swallowed. The timestamped format in the output assumes logging was configured first (see step 3); without basicConfig you still get the message, just without the timestamp.
2. Define a domain exception
python -c "class ConfigError(Exception): pass"Expected output:
(no output; ConfigError defined)
What this teaches: A domain exception like ConfigError lets callers distinguish 'bad config' from 'network down' and handle each appropriately.
3. Configure logging output
python -c "import logging; logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')"Expected output:
(no output; logging configured)
What this teaches: basicConfig with a timestamped format turns log lines into greppable, sortable records instead of bare prints that vanish into the void. Call it once, before any log call, because a second basicConfig after handlers exist is silently ignored.
How It Works
How It Works
Targeted except clauses are a contract: they declare exactly which failures this code understands. A bare except also catches KeyboardInterrupt, SystemExit, and programming bugs, hiding the failures you most need to see.
Custom exceptions create a vocabulary for your domain. Raising ConfigError("missing key: api_url") tells the caller what went wrong in domain terms, while the original exception chained with raise ... from e preserves the technical cause.
Logging beats print because it carries severity, timestamps, and routing. Levels let operators filter noise (INFO in development, WARNING in production), and handlers route the same records to stderr, files, or aggregation systems without code changes.
this failure?"} K -->|"yes, recoverable"| H["handle: retry / default / cleanup"] K -->|"yes, fatal"| L["log with context,
then re-raise"] K -->|"no"| P["let it propagate
with traceback"] H --> C["cleanup in finally
or context manager"]
Validation & Testing
Force each failure mode (timeout, bad input, missing file) and confirm the right handler runs, cleanup always executes, and the fault is logged with context. Pass criteria: no bare excepts, resources released, and every error logged.
Advanced: Troubleshooting
- Swallowed errors: never
except: pass; at minimum log and re-raise. - Lost traceback: use
log.exception()inside an except to capture the stack. - Resource leaks: prefer context managers or
finallyfor cleanup.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| traceback shows 'During handling of the above exception' | an exception was raised inside an except block | chain deliberately with raise ... from e or fix the handler |
| logs never appear | basicConfig was called after a handler already existed, or level is too high | configure logging first, before any log call; check the effective level |
| except order never reaches the specific clause | a broad Exception clause sits before the specific one | order handlers from most specific to most general |
| resources leak on failure | cleanup code sat after the raising call instead of in finally | move cleanup into finally or a with block |
| custom exception loses the cause | raised without chaining the original | use raise ConfigError(...) from e to preserve the chain |
Key Results
- Replaced broad excepts with specific handling across the codebase.
- Logged 100% of faults with timestamp, level, and context.
- Guaranteed resource cleanup via finally and context managers.
- Introduced custom exceptions for clear domain error signaling.
Key Takeaways
- Catch specific exceptions; bare except hides the failures you need to see.
- Domain exceptions give failures a vocabulary callers can act on.
- Logging with levels and timestamps beats print for anything operational.
- Cleanup belongs in finally or context managers, not after the risky call.
- When in doubt, log with context and re-raise rather than swallowing.
Practice Challenges
Challenge 1: Retry with backoff
Goal: Combine the retry decorator from the functions lab with logging so each attempt is logged at WARNING before the final raise.
Hint: Log inside the except before sleeping; re-raise the last exception after attempts run out.
Challenge 2: Config loader
Goal: Write a loader that raises ConfigError with the missing key name for absent keys and the file path for unreadable files.
Hint: Catch FileNotFoundError and KeyError separately and chain with from.