Chapter 12: Errors, Exceptions, and Debugging — Exercises¶
Overview¶
These exercises help you understand Python's error handling, write robust exception handlers, and debug code effectively. By the end, you will write defensive code that handles errors gracefully.
How to Use These Exercises¶
- Create a folder called
chapter-12in yourpython-learningdirectory. - Write each program in a separate
.pyfile. - Run each program and observe the output.
- Experiment with variations to deepen your understanding.
Warm-up Exercises¶
Exercise 1: Catch and Handle Exceptions¶
Create a file called exception_handling.py:
# Handle ValueError
try:
number = int("not a number")
except ValueError:
print("Error: Could not convert to integer")
# Handle KeyError
try:
d = {"a": 1, "b": 2}
print(d["c"])
except KeyError:
print("Error: Key not found")
# Handle IndexError
try:
lst = [1, 2, 3]
print(lst[10])
except IndexError:
print("Error: Index out of range")
# Handle multiple exceptions
try:
value = int(input("Enter a number: "))
result = 10 / value
print(f"Result: {result}")
except ValueError:
print("Error: Not a valid number")
except ZeroDivisionError:
print("Error: Cannot divide by zero")
# Handle any exception
try:
x = 1 / 0
except Exception as e:
print(f"Error: {e}")
Run it and observe exception handling.
Exercise 2: Use try-except-else-finally¶
Create a file called try_except_else_finally.py:
# try-except-else
try:
number = int("42")
except ValueError:
print("Error: Invalid number")
else:
print(f"Success: {number}")
# try-except-finally
try:
file = open("nonexistent.txt", "r")
except FileNotFoundError:
print("Error: File not found")
finally:
print("Cleanup: This always runs")
# try-except-else-finally
try:
value = int(input("Enter a number: "))
except ValueError:
print("Error: Not a valid number")
else:
print(f"You entered: {value}")
finally:
print("Done")
# Proper file handling
try:
with open("test.txt", "w") as f:
f.write("Hello, World!")
print("File written successfully")
except IOError as e:
print(f"Error: {e}")
finally:
print("File operation complete")
Run it and observe the flow of try-except-else-finally.
Exercise 3: Raise Exceptions¶
Create a file called raise_exceptions.py:
# Raise a built-in exception
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return age
try:
validate_age(-5)
except ValueError as e:
print(f"Error: {e}")
# Raise with a message
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")
# Re-raise an exception
try:
try:
x = 1 / 0
except ZeroDivisionError:
print("Caught division by zero")
raise # Re-raise the same exception
except ZeroDivisionError:
print("Exception propagated")
# Chain exceptions
try:
try:
int("not a number")
except ValueError as e:
raise RuntimeError("Failed to parse input") from e
except RuntimeError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
Run it and observe exception raising.
Exercise 4: Debug with Print Statements¶
Create a file called debug_print.py:
def calculate_average(numbers):
print(f"Input: {numbers}")
total = sum(numbers)
print(f"Total: {total}")
count = len(numbers)
print(f"Count: {count}")
average = total / count
print(f"Average: {average}")
return average
result = calculate_average([10, 20, 30, 40])
print(f"Result: {result}")
# Debug with type checking
def process_data(data):
print(f"Type of data: {type(data)}")
print(f"Data: {data}")
if isinstance(data, list):
return sum(data)
elif isinstance(data, dict):
return sum(data.values())
else:
print("Unexpected type")
return None
print(process_data([1, 2, 3]))
print(process_data({"a": 1, "b": 2}))
Run it and observe debugging with print statements.
Practice Exercises¶
Exercise 5: Validate User Input¶
Create a file called input_validation.py:
def get_positive_integer(prompt):
"""Get a positive integer from user."""
while True:
try:
value = int(input(prompt))
if value <= 0:
print("Error: Must be positive")
continue
return value
except ValueError:
print("Error: Not a valid integer")
def get_choice(options):
"""Get a choice from a list of options."""
while True:
try:
choice = int(input(f"Choose (1-{len(options)}): "))
if choice < 1 or choice > len(options):
print(f"Error: Choose between 1 and {len(options)}")
continue
return choice - 1
except ValueError:
print("Error: Not a valid number")
# Use the validators
# age = get_positive_integer("Enter your age: ")
# print(f"You are {age} years old")
# options = ["Option A", "Option B", "Option C"]
# choice = get_choice(options)
# print(f"You chose: {options[choice]}")
Run it and test the validators.
Exercise 6: Handle File Operations¶
Create a file called file_operations.py:
# Safe file reading
def read_file_safe(filename):
try:
with open(filename, "r") as f:
return f.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found")
return None
except IOError as e:
print(f"Error reading file: {e}")
return None
# Safe file writing
def write_file_safe(filename, content):
try:
with open(filename, "w") as f:
f.write(content)
print(f"File '{filename}' written successfully")
except IOError as e:
print(f"Error writing file: {e}")
# Safe JSON operations
import json
def load_json_safe(filename):
try:
with open(filename, "r") as f:
return json.load(f)
except FileNotFoundError:
print(f"Error: File '{filename}' not found")
return None
except json.JSONDecodeError:
print(f"Error: Invalid JSON in '{filename}'")
return None
def save_json_safe(filename, data):
try:
with open(filename, "w") as f:
json.dump(data, f, indent=2)
print(f"JSON saved to '{filename}'")
except IOError as e:
print(f"Error writing JSON: {e}")
# Test
write_file_safe("test.txt", "Hello, World!")
content = read_file_safe("test.txt")
print(f"Content: {content}")
Run it and observe file handling.
Exercise 7: Create Custom Exceptions¶
Create a file called custom_exceptions.py:
# Define custom exceptions
class InvalidAgeError(Exception):
"""Raised when age is invalid."""
pass
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds."""
pass
# Use custom exceptions
def validate_age(age):
if age < 0 or age > 150:
raise InvalidAgeError(f"Age {age} is invalid")
return age
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount} from balance {balance}"
)
return balance - amount
# Handle custom exceptions
try:
validate_age(-5)
except InvalidAgeError as e:
print(f"Age error: {e}")
try:
new_balance = withdraw(100, 150)
except InsufficientFundsError as e:
print(f"Funds error: {e}")
# Custom exception with additional info
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
try:
raise ValidationError("email", "Invalid email format")
except ValidationError as e:
print(f"Validation error: {e}")
Run it and observe custom exceptions.
Challenge Exercises¶
Challenge 1: Build a Robust Calculator¶
Create a file called robust_calculator.py:
def safe_divide(a, b):
"""Divide with error handling."""
try:
return a / b
except ZeroDivisionError:
print("Error: Cannot divide by zero")
return None
except TypeError:
print("Error: Invalid operand types")
return None
def safe_calculate(expression):
"""Evaluate expression with error handling."""
try:
result = eval(expression)
return result
except ZeroDivisionError:
print("Error: Division by zero")
return None
except NameError:
print("Error: Undefined variable")
return None
except SyntaxError:
print("Error: Invalid expression")
return None
# Test
print(safe_divide(10, 2))
print(safe_divide(10, 0))
print(safe_calculate("10 + 5"))
print(safe_calculate("10 / 0"))
Run it and test the calculator.
Challenge 2: Implement Retry Logic¶
Create a file called retry_logic.py:
def retry(func, max_attempts=3, delay=1):
"""Retry a function with exponential backoff."""
import time
for attempt in range(1, max_attempts + 1):
try:
return func()
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
time.sleep(delay)
def unreliable_function():
"""Simulates an unreliable operation."""
import random
if random.random() < 0.7:
raise RuntimeError("Random failure")
return "Success!"
# Test
try:
result = retry(unreliable_function, max_attempts=5)
print(f"Result: {result}")
except RuntimeError:
print("Operation failed")
Run it and observe retry logic.
Challenge 3: Log Errors to a File¶
Create a file called error_logging.py:
import traceback
from datetime import datetime
def log_error(filename, error):
"""Log an error to a file."""
try:
with open(filename, "a") as f:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"[{timestamp}] {error}\n")
f.write(traceback.format_exc())
f.write("-" * 50 + "\n")
except IOError as e:
print(f"Could not log error: {e}")
def risky_operation():
"""Simulate a risky operation."""
x = 1 / 0
# Test
try:
risky_operation()
except Exception as e:
log_error("errors.log", str(e))
print("Error logged")
# Read the log
try:
with open("errors.log", "r") as f:
print("\nError log:")
print(f.read())
except FileNotFoundError:
print("No error log found")
Run it and observe error logging.
Hints¶
Exception not caught → Make sure the exception type matches. Use except Exception to catch all exceptions.
Finally block not running → The finally block always runs, even if an exception is raised or caught.
Bare except is bad → Avoid except: without a type. Use except Exception: instead.
Exception message unclear → Include context in your exception messages to help with debugging.
What to Review If You Get Stuck¶
- Exception types → Handbook section 2.1
- try-except blocks → Handbook section 2.2
- try-except-else-finally → Handbook section 2.3
- Raising exceptions → Handbook section 2.4
- Custom exceptions → Handbook section 2.5
- Debugging techniques → Handbook section 2.6
Key Takeaways¶
After completing these exercises, you should be able to:
- Catch and handle exceptions with try-except
- Use try-except-else-finally effectively
- Raise exceptions with meaningful messages
- Create custom exception classes
- Validate user input safely
- Handle file operations robustly
- Debug code with print statements and exception info
- Log errors for later analysis