Exercises: Error Handling

These exercises progress from reading tracebacks to designing complete error handling strategies. Every coding exercise should produce runnable Python 3.12+ code.

Difficulty Guide: - ⭐ Foundational (5-10 min each) - ⭐⭐ Intermediate (10-20 min each) - ⭐⭐⭐ Challenging (20-40 min each) - ⭐⭐⭐⭐ Advanced/Research (40+ min each)


Part A: Reading Tracebacks ⭐

A.1. The following traceback appears when running a program. Answer these questions without running any code:

Traceback (most recent call last):
  File "report.py", line 22, in <module>
    generate_report("sales_q4.csv")
  File "report.py", line 15, in generate_report
    total = compute_total(rows)
  File "report.py", line 8, in compute_total
    amount = float(row[2])
ValueError: could not convert string to float: 'N/A'

a) What is the exception type? b) What value caused the error? c) Which function raised the exception? d) Which function called it? e) On which line did the program start the chain that led to the crash?

A.2. Classify each of the following as a syntax error, runtime error, or logic error:

a) for i in range(10) (missing colon) b) A function that computes the area of a circle using 2 * 3.14 * r instead of 3.14 * r ** 2 c) names = ["Alice"]; print(names[3]) d) print("Hello world') e) int("3.14") (hint: try it in the REPL) f) A sorting function that returns a list in descending order when ascending was intended

A.3. For each exception type, write one line of Python code that would trigger it. Do not use raise.

a) ValueError b) TypeError c) IndexError d) KeyError e) ZeroDivisionError f) FileNotFoundError


Part B: Basic try/except ⭐⭐

B.1. Write a function safe_divide(a, b) that returns the result of a / b. If b is zero, it should return None instead of crashing. If either argument isn't a number, it should also return None.

# Expected behavior:
print(safe_divide(10, 3))      # 3.3333333333333335
print(safe_divide(10, 0))      # None
print(safe_divide("ten", 2))   # None

B.2. Write a function safe_int(value, default=0) that tries to convert value to an integer. If conversion fails, it returns default.

# Expected behavior:
print(safe_int("42"))       # 42
print(safe_int("hello"))    # 0
print(safe_int("", -1))    # -1
print(safe_int(3.7))        # 3

B.3. Write a function get_float_input(prompt) that keeps asking the user for input until they enter a valid float. It should print a helpful message on invalid input and re-prompt.

B.4. Write a function safe_list_access(lst, index, default=None) that returns lst[index] if the index is valid, or default if an IndexError occurs. Use EAFP, not LBYL.

# Expected behavior:
colors = ["red", "green", "blue"]
print(safe_list_access(colors, 1))           # green
print(safe_list_access(colors, 10))          # None
print(safe_list_access(colors, 10, "N/A"))   # N/A

B.5. Elena's nonprofit script reads CSV lines in the format "name,score". Write a function parse_record(line) that: - Splits the line on commas - Converts the second field to a float - Returns a tuple (name, score) - Raises ValueError with a descriptive message if the line doesn't have exactly two fields or the score isn't numeric

# Expected behavior:
print(parse_record("Alice,95.5"))    # ('Alice', 95.5)
print(parse_record("Bob,87"))        # ('Bob', 87.0)
# parse_record("Charlie")           # ValueError: Expected 2 fields, got 1: 'Charlie'
# parse_record("Dana,abc")          # ValueError: Invalid score 'abc' for student 'Dana'

Part C: Multiple Exceptions and Full try/except/else/finally ⭐⭐

C.1. Write a function read_number_from_file(filename, line_num) that: - Opens the specified file - Reads the line at position line_num (0-based) - Converts that line to a float and returns it - Handles FileNotFoundError, IndexError, and ValueError with distinct messages - Uses else to print a success message - Uses finally to print "Operation complete."

C.2. Write a function process_scores(score_strings) that takes a list of strings (e.g., ["85", "92", "abc", "78", ""]), converts each to an integer, and returns a list of the successfully converted values. Track and report how many values were skipped due to errors.

# Expected behavior:
result = process_scores(["85", "92", "abc", "78", ""])
# Output: Skipped 2 invalid score(s).
# result == [85, 92, 78]

C.3. Predict the output of this code, then verify by running it:

def mystery(x):
    try:
        result = 10 / x
    except ZeroDivisionError:
        print("A")
        return -1
    except TypeError:
        print("B")
        return -2
    else:
        print("C")
    finally:
        print("D")
    return result

print(mystery(2))
print("---")
print(mystery(0))
print("---")
print(mystery("five"))

C.4. Write a function safe_json_load(filepath) that: - Attempts to open and parse a JSON file - Returns the parsed data on success - Returns an empty dict {} on FileNotFoundError - Returns an empty dict {} on json.JSONDecodeError, but also prints a warning - Uses finally to print the filepath that was attempted (for logging purposes)

C.5. Write a transfer_funds(accounts, from_acct, to_acct, amount) function where accounts is a dict of {"name": balance}. The function should: - Raise KeyError if either account doesn't exist - Raise ValueError if the amount is negative or if the source account has insufficient funds - Perform the transfer atomically — if any check fails, neither balance should change


Part D: Raising Exceptions ⭐⭐

D.1. Write a function validate_email(email) that raises ValueError with a descriptive message if: - The input is not a string - The string is empty - The string doesn't contain exactly one @ symbol - There's no . after the @ symbol

If valid (by these simple rules), return the email in lowercase.

D.2. Write a function create_student(name, age, gpa) that: - Raises TypeError if name isn't a string - Raises ValueError if name is empty or only whitespace - Raises TypeError if age isn't an int - Raises ValueError if age is not between 16 and 120 - Raises TypeError if gpa isn't an int or float - Raises ValueError if gpa is not between 0.0 and 4.0 - Returns a dict {"name": name.strip(), "age": age, "gpa": float(gpa)} if all checks pass

Write at least 5 test calls that demonstrate both valid input and each type of validation error.

D.3. Rewrite the grade calculator function from the chapter so that it raises InvalidScoreError (a custom exception you define) instead of the generic ValueError. The error message should include the invalid score and the valid range.

D.4. Write a function withdraw(balance, amount) that: - Raises TypeError if either argument isn't a number - Raises ValueError("Withdrawal amount must be positive") if amount <= 0 - Raises ValueError("Insufficient funds: ...") if amount > balance, including both the balance and amount in the message - Returns the new balance if successful


Part E: LBYL vs EAFP ⭐⭐⭐

E.1. Convert each LBYL snippet to EAFP style:

a)

if key in my_dict:
    value = my_dict[key]
else:
    value = "default"

b)

if len(my_list) > 0:
    first = my_list[0]
else:
    first = None

c)

import os
if os.path.exists(filename):
    with open(filename) as f:
        data = f.read()
else:
    data = ""

E.2. For each scenario, argue whether LBYL or EAFP is the better choice and explain why:

a) Checking if a user-provided string can be converted to an integer b) Checking if a file exists before beginning a 30-minute processing job c) Accessing a dictionary key that should almost always be present d) Validating 10,000 rows of CSV data where about 20% have formatting errors

E.3. Write two versions of a function lookup_student(database, student_id) where database is a dict — one using LBYL and one using EAFP. Then write a paragraph explaining which you'd prefer and why.


Part F: Custom Exceptions ⭐⭐⭐

F.1. Create an exception hierarchy for a library management system: - LibraryError (base) - BookNotFoundError - BookAlreadyCheckedOutError - OverdueLimitError

Write a checkout_book(library, patron, book_id) function that uses these exceptions. The library parameter is a dict mapping book_id to {"title": str, "checked_out_by": str or None}.

F.2. Create a PasswordValidationError exception that stores which specific rules were violated. The exception should accept a list of violation strings. Write a validate_password(password) function that checks at least 4 rules (length, uppercase, digit, special character) and raises the exception with all violations listed.

# Expected behavior:
try:
    validate_password("hi")
except PasswordValidationError as e:
    print(e)
# Output something like:
# Password validation failed:
#   - Must be at least 8 characters (got 2)
#   - Must contain at least one uppercase letter
#   - Must contain at least one digit
#   - Must contain at least one special character

Part G: Integration and Design ⭐⭐⭐⭐

G.1. Write a complete program grade_processor.py that: - Reads a CSV file where each line is student_name,score - Validates each line (non-empty name, score between 0-100) - Skips invalid lines with a warning (don't crash) - Calculates and prints the average of all valid scores - Handles: file not found, empty file, file with no valid scores - Uses at least one custom exception

Test with a file that contains a mix of valid and invalid lines.

G.2. Design the error handling strategy for a simple ATM simulation. The ATM should support: check balance, deposit, withdraw, and quit. Think about: - What can go wrong with each operation? - What exception types would you use? - Would you use LBYL or EAFP for each case? - What custom exceptions would be helpful?

Write the complete program with at least 3 custom exceptions, input validation with retry loops, and file-based persistence for account balances.

G.3. Extend TaskFlow v1.0 with a search_tasks(tasks, keyword) function that: - Searches task descriptions for the keyword (case-insensitive) - Raises ValueError if the keyword is empty or only whitespace - Raises TypeError if tasks is not a list - Returns a list of matching tasks (could be empty — that's not an error) - Add this as option 6 in the main menu with appropriate error handling

G.4. (Research) Read about Python's contextlib.suppress context manager. Rewrite three examples from this chapter using suppress instead of try/except/pass. When is suppress more readable? When is explicit try/except better?