Chapter 8 Exercises: Error Handling
These exercises are organized into five tiers. Work through them in order — each tier builds on the previous one. All exercises use realistic business scenarios.
Tier 1: Foundations (Understanding the Basics)
These exercises confirm you understand what exceptions are, how to read a traceback, and how to write basic try/except blocks.
Exercise 1.1 — Read the Traceback
The following code raises an exception. Without running it, read the traceback below it and answer the questions.
def get_commission(sales_data, rep_name):
sales = sales_data[rep_name]
commission = sales * 0.08
return commission
quarterly_data = {
"Sandra Chen": 125000,
"James Park": 98000,
}
rep = "Marcus Webb"
amount = get_commission(quarterly_data, rep)
print(f"Commission: ${amount:.2f}")
Traceback (most recent call last):
File "commissions.py", line 11, in <module>
amount = get_commission(quarterly_data, rep)
File "commissions.py", line 2, in get_commission
sales = sales_data[rep_name]
KeyError: 'Marcus Webb'
Answer these questions in comments in your code file:
a) What type of exception was raised? b) On which line of the original code did the error occur? c) What caused the exception? (In plain English — one sentence.) d) What is the name of the variable that caused the problem, and what value did it hold? e) What would be a sensible way to handle this exception? (Describe in words — no code needed yet.)
Exercise 1.2 — Basic try/except
The function below crashes when revenue_str cannot be converted to a float. Add a try/except block so that if the conversion fails, the function prints a warning message and returns 0.0 instead of crashing.
def parse_monthly_revenue(revenue_str: str) -> float:
"""Convert a revenue string to float."""
revenue = float(revenue_str)
return revenue
# Test your solution with these calls:
print(parse_monthly_revenue("45000.00")) # Should print: 45000.0
print(parse_monthly_revenue("N/A")) # Should print a warning and return 0.0
print(parse_monthly_revenue("")) # Should print a warning and return 0.0
print(parse_monthly_revenue("$1,200")) # This one WILL fail — that's OK for now
Exercise 1.3 — Match the Exception Type
Draw a line connecting each code snippet on the left to the exception it raises on the right.
| Code Snippet | Exception Type |
|---|---|
float("pending") |
IndexError |
{"Q1": 45000}["Q2"] |
TypeError |
"hello" + 5 |
ZeroDivisionError |
[10, 20, 30][99] |
KeyError |
100 / 0 |
ValueError |
After matching them, write a short try/except block for each one that catches the correct exception and prints a helpful message.
Exercise 1.4 — The Danger of bare except
The following code uses a bare except. Identify two specific problems this creates, then rewrite it with a proper except ValueError: clause.
def calculate_discount(price_str: str, discount_pct: str) -> float:
try:
price = float(price_str)
discount = float(discount_pct)
return price * (1 - discount)
except:
return 0.0
Hint: Think about what happens if (a) there is a typo in the function that causes a NameError, or (b) the user presses Ctrl+C while the function is running.
Exercise 1.5 — The Exception Hierarchy
Without running any code, answer these questions about Python's exception hierarchy:
a) If you write except LookupError:, will it catch a KeyError? Why or why not?
b) If you write except Exception:, will it catch KeyboardInterrupt? Why or why not?
c) You have two except clauses: except ValueError and except Exception. Which one should come first and why?
d) Write the class definition line for a custom exception called InvoiceError that inherits from Exception.
Tier 2: Applied Handling (Working with Real Scenarios)
These exercises apply error handling to realistic business data problems.
Exercise 2.1 — File Loader with Fallback
Write a function load_budget_file(filepath: str) -> list[dict] that:
- Opens a CSV file and returns its rows as a list of dictionaries
- Catches
FileNotFoundErrorand prints a clear message including the filename - Catches
PermissionErrorand tells the user to close the file in Excel - Returns an empty list
[]in all error cases (rather than crashing)
Test it by calling it with a path to a file that does not exist.
Exercise 2.2 — Multiple except Clauses
The following function handles an invoice row but currently has no error handling. Add except clauses for at least three different exception types, each with a different message. After adding error handling, the function should return None on failure instead of crashing.
def process_invoice_row(row: dict) -> dict | None:
invoice_id = row["invoice_id"] # Could raise KeyError
amount = float(row["amount"]) # Could raise ValueError
units = int(row["units"]) # Could raise ValueError
unit_price = amount / units # Could raise ZeroDivisionError
return {
"invoice_id": invoice_id,
"amount": amount,
"units": units,
"unit_price": unit_price,
}
Exercise 2.3 — The else Clause
Refactor the function below to use a try/except/else structure. Move the code that should only run on success into the else clause. Add a brief comment explaining why each block of code is in its location.
def apply_discount(price_str: str, rate_str: str) -> float | None:
try:
price = float(price_str)
rate = float(rate_str)
discounted = price * (1 - rate)
if discounted < 0:
raise ValueError("Discounted price cannot be negative")
print(f"Discount applied: ${price:.2f} → ${discounted:.2f}")
return discounted
except ValueError as e:
print(f"Error: {e}")
return None
Exercise 2.4 — The finally Clause
The function below opens a file but might not close it properly if an exception occurs. Rewrite it using a finally clause to guarantee the file is always closed. (Do not use a with statement — the point of this exercise is to practice finally.)
def count_invoice_lines(filepath: str) -> int:
f = open(filepath, encoding="utf-8")
reader = csv.reader(f)
next(reader) # Skip header
count = sum(1 for _ in reader)
f.close()
return count
Exercise 2.5 — Raising Your Own Exceptions
Write a function validate_tax_rate(rate: float) -> None that:
- Raises a
ValueErrorwith a descriptive message ifrateis not between 0.0 and 1.0 (inclusive) - Raises a
TypeErrorwith a descriptive message ifrateis not afloatorint - Does nothing (returns
None) if the value is valid
Write three test calls: one that passes, one that raises ValueError, and one that raises TypeError.
Tier 3: Intermediate (Logging and Custom Exceptions)
These exercises introduce the logging module and custom exception classes.
Exercise 3.1 — Replace print() with logging
The following script uses print() for all output. Refactor it to use the logging module. Use appropriate log levels: DEBUG for step-by-step details, INFO for normal progress, WARNING for skipped rows, and ERROR for failures.
def process_sales_batch(rows):
print("Starting batch processing...")
results = []
skipped = 0
for i, row in enumerate(rows):
print(f"Processing row {i+1}...")
try:
amount = float(row["amount"])
results.append(amount)
print(f" Row {i+1}: ${amount:.2f} — OK")
except ValueError:
skipped += 1
print(f" Row {i+1}: bad amount '{row['amount']}' — skipping")
print(f"Done. Processed {len(results)}, skipped {skipped}.")
return results
Your refactored version should also configure basicConfig to include timestamps in the log output.
Exercise 3.2 — Logging to a File
Extend your solution from Exercise 3.1 to log to both the console and a file called batch_processing.log. The console should show INFO level and above; the file should capture everything including DEBUG.
Bonus: Add a log message at CRITICAL level if the entire batch was skipped (zero rows processed).
Exercise 3.3 — Build a Custom Exception Hierarchy
Create a three-level exception hierarchy for a payroll processing system:
PayrollError (base — inherits from Exception)
├── EmployeeNotFoundError
├── InvalidSalaryError
│ (stores: employee_id, salary_value, reason)
└── PayPeriodError
Each exception should:
- Have an __init__ that calls super().__init__() with a clear message
- Store relevant attributes so calling code can access them
- Include a docstring explaining when it is raised
Write a demonstration showing each exception being raised and caught.
Exercise 3.4 — Logging Exceptions with Tracebacks
Write a function safe_calculate_ytd(employee_records: list[dict]) -> float that:
- Loops over employee records
- Tries to sum the
ytd_salaryfield for each record - Uses
logger.exception()to log any exception with its full traceback - Continues to the next record after logging an error (does not crash)
- Returns the total of valid records
Demonstrate that logger.exception() captures the traceback in the log output.
Exercise 3.5 — Audit Log Pattern
Many business systems require an "audit log" — a record of every action taken, including errors. Write a function process_with_audit(records: list[dict], action_fn: callable) -> dict that:
- Calls
action_fn(record)for each record - Logs a
SUCCESSorFAILUREentry for each record - Returns a summary dictionary:
{"processed": N, "failed": N, "errors": [...]} - Uses the
loggingmodule throughout
Tier 4: Advanced (Defensive Design and Graceful Degradation)
These exercises focus on designing systems that handle failure gracefully at multiple levels.
Exercise 4.1 — Multi-Level Error Boundaries
Acme Corp's data pipeline has three stages: load, transform, export. Write a function for each stage and a run_pipeline(source_path, output_path) function that:
- Calls each stage inside its own
try/exceptblock - If loading fails, logs the error and returns immediately (cannot continue)
- If transforming partially fails (some rows bad), logs and continues with valid rows
- If exporting fails, logs the error but still returns the transformation result
- Uses a
ProcessingResultdataclass to track counts and errors
Exercise 4.2 — The Robust CSV Loop Pattern
Acme Corp receives daily sales data from five regional distributors. Each distributor file has the same structure but comes from a different system (some have encoding quirks; some occasionally have corrupt rows). Write a process_distributor_files(file_paths: list[str]) -> dict function that:
- Loads each file (handles
FileNotFoundError,UnicodeDecodeError) - Processes each row (handles
ValueError,KeyError) - Returns a dictionary mapping distributor name to its total revenue
- Missing distributors map to
None(not 0.0 — the distinction matters) - Writes a summary to
distributor_report.log
Exercise 4.3 — Re-raising with Context
Python 3 allows you to chain exceptions: raise NewException("message") from original_exception. This preserves the original traceback in the chain.
Study this pattern:
def load_config(filepath: str) -> dict:
try:
with open(filepath) as f:
return json.load(f)
except FileNotFoundError as e:
raise RuntimeError(
f"Application cannot start: config file '{filepath}' is missing"
) from e
Now write a load_pricing_rules(filepath: str) -> dict function that:
- Wraps
FileNotFoundErrorin aConfigurationError(which you define) - Wraps
json.JSONDecodeErrorin aConfigurationErrorwith an explanation about JSON syntax - In both cases, chains the original exception using
from
Demonstrate the output by catching ConfigurationError and printing both the new message and exc.__cause__.
Exercise 4.4 — Input Validation Before Processing
Write a function validate_payroll_batch(records: list[dict]) -> tuple[list[dict], list[str]] that:
- Validates every record before processing begins
- Returns
(valid_records, error_messages)— a tuple of clean records and a list of error strings - Does NOT process anything — only validates
- Checks: employee_id is non-empty, salary is positive float, department is in
{"SALES", "ENGINEERING", "FINANCE", "HR", "OPS"}, start_date matches"%Y-%m-%d"
Then write a separate process_valid_payroll(valid_records: list[dict]) -> float that processes the already-validated records. (By separating validation from processing, you make both functions simpler and more testable.)
Exercise 4.5 — Context Managers for Resource Safety
The finally clause is one way to guarantee cleanup. Another is writing a context manager. Write a context manager class ManagedReport that:
- Opens a report file when entering the context
- Writes a header row
- Yields the file handle for use in the
withblock - Always writes a footer row and closes the file when exiting the context, even if an exception occurred inside the
withblock - Logs any exception that occurred inside the context (but re-raises it)
Usage should look like:
with ManagedReport("monthly_report.csv") as report:
for row in data:
report.write(row)
Tier 5: Real-World Challenges
These open-ended exercises simulate actual business problems. There is no single correct solution — focus on clean error handling, appropriate logging, and graceful degradation.
Exercise 5.1 — Quarterly Budget Reconciliation
Priya receives two files every quarter: budget.csv (planned spending per department) and actuals.csv (actual spending). She needs to calculate variance for each department. The challenge: the two files may not have matching departments, and either file may have missing or corrupt values.
Write a complete reconcile_budgets(budget_path, actuals_path) function that:
- Loads both files with proper error handling
- Matches rows by department (handles missing departments in either file)
- Calculates variance for matching departments, handles bad numeric data row by row
- Produces a clean output CSV and a log file
- Returns a summary with counts and total variance
Evaluation criteria: Does it produce partial output when one file has problems? Does it distinguish "department missing from actuals" from "department has corrupt data in actuals"?
Exercise 5.2 — Maya's Batch Invoice Processor
Maya now has five clients sending timesheets simultaneously at month-end. She wants a script that processes all five, produces individual invoice files, and sends her a summary of any that had issues.
Write process_all_invoices(timesheet_directory: str, output_directory: str) that:
- Discovers all
.csvfiles intimesheet_directory - Processes each as an invoice, handling all errors per Chapter 8 patterns
- Writes individual invoice text files to
output_directory - Writes a master
invoice_summary.log - Returns a summary dictionary:
{"succeeded": [...], "partial": [...], "failed": [...]}
Evaluation criteria: Does a completely failed invoice prevent the others from running? Does a partially failed invoice (some bad rows) still produce output?
Exercise 5.3 — API Response Validator
Marcus at Acme Corp has written code that calls an internal REST API to get customer data. The API occasionally returns unexpected response shapes. Write a function parse_customer_api_response(response_json: dict) -> Customer (where Customer is a dataclass you define) that:
- Validates the required fields exist in the response
- Validates the types and ranges of numeric fields
- Raises a custom
APIResponseErrorwith a clear message for any validation failure - Is robust to
Nonevalues vs. missing keys (these may need different error messages)
Evaluation criteria: Does it distinguish "key missing from response" from "key present but value is None" from "key present but value is wrong type"?
Exercise 5.4 — Retry Logic for Transient Failures
Network and API calls sometimes fail temporarily — the server is briefly overloaded, or there is a momentary network blip. Write a generic retry(fn, max_attempts=3, delay_seconds=1.0, retryable_exceptions=(Exception,)) decorator that:
- Calls
fn() - If it raises an exception in
retryable_exceptions, waitsdelay_secondsand tries again - After
max_attemptsfailures, raises the last exception - Logs each attempt (attempt number, exception type, message)
- Does NOT retry on non-retryable exceptions (e.g.,
ValueErrorfor bad data should not be retried — retrying won't fix bad data)
Evaluation criteria: Does it distinguish transient failures (retry makes sense) from permanent failures (retry is pointless)?
Exercise 5.5 — Design Exercise: Exception Architecture for a New Module
You are building a Python module for Acme Corp that processes purchase orders. Before writing any processing code, design the exception hierarchy for the module.
Write a document (as comments or a separate .md file) that specifies:
- The base exception class and when each sub-exception is raised
- Which exceptions are "fatal" (the whole batch must stop) vs. "row-level" (skip and continue)
- What data each exception should carry as attributes
- How the calling code is expected to handle each exception
Then implement the exception hierarchy as Python classes.
Evaluation criteria: Does the hierarchy match the business domain? Is there a clear distinction between "programming errors" (bugs) and "data errors" (bad input)?
Answer Guidance
Tier 1 exercises can typically be completed in 10–20 lines of code each. Focus on correctness of exception types and message quality.
Tier 2 exercises push toward realistic patterns. The key question after each one: "Would this function help someone diagnose a problem, or would it just say 'something went wrong'?"
Tier 3 exercises introduce the logging module. After completing 3.1 and 3.2, you should never use print() for error messages in production code again.
Tier 4 exercises are design-heavy. The value is not just getting the code to work — it is thinking through where error boundaries belong and why.
Tier 5 exercises are open-ended. A solution that handles 80% of edge cases gracefully and documents the remaining 20% is better than a solution that tries to handle everything but crashes on unusual inputs.