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 FileNotFoundError and prints a clear message including the filename
  • Catches PermissionError and 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 ValueError with a descriptive message if rate is not between 0.0 and 1.0 (inclusive)
  • Raises a TypeError with a descriptive message if rate is not a float or int
  • 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_salary field 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 SUCCESS or FAILURE entry for each record
  • Returns a summary dictionary: {"processed": N, "failed": N, "errors": [...]}
  • Uses the logging module 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/except block
  • 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 ProcessingResult dataclass 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 FileNotFoundError in a ConfigurationError (which you define)
  • Wraps json.JSONDecodeError in a ConfigurationError with 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 with block
  • Always writes a footer row and closes the file when exiting the context, even if an exception occurred inside the with block
  • 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 .csv files in timesheet_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 APIResponseError with a clear message for any validation failure
  • Is robust to None values 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, waits delay_seconds and tries again
  • After max_attempts failures, raises the last exception
  • Logs each attempt (attempt number, exception type, message)
  • Does NOT retry on non-retryable exceptions (e.g., ValueError for 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:

  1. The base exception class and when each sub-exception is raised
  2. Which exceptions are "fatal" (the whole batch must stop) vs. "row-level" (skip and continue)
  3. What data each exception should carry as attributes
  4. 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.