Python Basics — Error Handling Exercises
Practice Exercises — Error Handling
Test your understanding of exceptions, try/except, raising errors, custom exceptions, and debugging.
Exercise 1: Safe Calculator
Write a safe_calculate(expression) function that takes a string like "10 / 3" and returns the result. Handle all possible errors gracefully: invalid operators, division by zero, non-numeric values.
Test with: "10 + 5", "10 / 0", "abc + 3", "10 % 3", "hello"
Show Solution
def safe_calculate(expression):
"""Evaluate a simple math expression safely."""
valid_operators = {"+", "-", "*", "/"}
try:
parts = expression.split()
if len(parts) != 3:
raise ValueError(f"Expected 'num op num', got: '{expression}'")
left_str, operator, right_str = parts
if operator not in valid_operators:
raise ValueError(f"Invalid operator: '{operator}' (use +, -, *, /)")
left = float(left_str)
right = float(right_str)
if operator == "+":
return left + right
elif operator == "-":
return left - right
elif operator == "*":
return left * right
elif operator == "/":
if right == 0:
raise ZeroDivisionError("Cannot divide by zero")
return left / right
except ValueError as e:
return f"Error: {e}"
except ZeroDivisionError as e:
return f"Error: {e}"
# Test
test_cases = ["10 + 5", "10 / 0", "abc + 3", "10 % 3", "hello"]
for expr in test_cases:
result = safe_calculate(expr)
print(f" {expr!r:20} → {result}")
Output:
'10 + 5' → 15.0
'10 / 0' → Error: Cannot divide by zero
'abc + 3' → Error: could not convert string to float: 'abc'
'10 % 3' → Error: Invalid operator: '%' (use +, -, *, /)
'hello' → Error: Expected 'num op num', got: 'hello'
Exercise 2: Retry Decorator
Write a retry function that takes a function and retries it up to max_attempts times if it raises an exception. After all attempts fail, raise the last exception.
Show Solution
import time
import random
def retry(func, max_attempts=3, delay=1):
"""Retry a function up to max_attempts times."""
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
result = func()
print(f" ✅ Attempt {attempt}: Success!")
return result
except Exception as e:
last_exception = e
print(f" ❌ Attempt {attempt}: {type(e).__name__}: {e}")
if attempt < max_attempts:
print(f" Retrying in {delay}s...")
time.sleep(delay)
raise last_exception
# Test with a flaky function
def flaky_api_call():
"""Simulates an unreliable API that fails 70% of the time."""
if random.random() < 0.7:
raise ConnectionError("Server unavailable")
return {"status": "ok", "data": [1, 2, 3]}
print("Testing retry mechanism:")
try:
result = retry(flaky_api_call, max_attempts=5, delay=0.5)
print(f" Final result: {result}")
except ConnectionError:
print(" All attempts failed!")
Exercise 3: Custom Exception Hierarchy
Create an exception hierarchy for a banking application:
BankError(base)AccountError:AccountNotFoundError,AccountLockedErrorTransactionError:InsufficientFundsError,DailyLimitError
Then write a transfer(from_acc, to_acc, amount) function that raises appropriate exceptions.
Show Solution
# Exception hierarchy
class BankError(Exception):
"""Base exception for banking operations."""
pass
class AccountError(BankError):
"""Account-related errors."""
pass
class AccountNotFoundError(AccountError):
"""Account does not exist."""
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"Account '{account_id}' not found")
class AccountLockedError(AccountError):
"""Account is locked."""
def __init__(self, account_id, reason="security"):
self.account_id = account_id
self.reason = reason
super().__init__(f"Account '{account_id}' is locked ({reason})")
class TransactionError(BankError):
"""Transaction-related errors."""
pass
class InsufficientFundsError(TransactionError):
"""Not enough funds for the transaction."""
def __init__(self, account_id, balance, amount):
self.account_id = account_id
self.balance = balance
self.amount = amount
super().__init__(
f"Insufficient funds in '{account_id}': "
f"balance=${balance:.2f}, requested=${amount:.2f}"
)
class DailyLimitError(TransactionError):
"""Daily transaction limit exceeded."""
def __init__(self, limit, attempted):
self.limit = limit
self.attempted = attempted
super().__init__(
f"Daily limit exceeded: limit=${limit:.2f}, attempted=${attempted:.2f}"
)
# Bank simulation
accounts = {
"ACC001": {"name": "Alice", "balance": 5000.00, "locked": False},
"ACC002": {"name": "Bob", "balance": 1200.00, "locked": False},
"ACC003": {"name": "Carol", "balance": 800.00, "locked": True},
}
DAILY_LIMIT = 10000.00
def transfer(from_id, to_id, amount):
"""Transfer money between accounts."""
# Validate accounts exist
if from_id not in accounts:
raise AccountNotFoundError(from_id)
if to_id not in accounts:
raise AccountNotFoundError(to_id)
from_acc = accounts[from_id]
to_acc = accounts[to_id]
# Check locks
if from_acc["locked"]:
raise AccountLockedError(from_id)
if to_acc["locked"]:
raise AccountLockedError(to_id, "receiving account locked")
# Check daily limit
if amount > DAILY_LIMIT:
raise DailyLimitError(DAILY_LIMIT, amount)
# Check funds
if from_acc["balance"] < amount:
raise InsufficientFundsError(from_id, from_acc["balance"], amount)
# Execute transfer
from_acc["balance"] -= amount
to_acc["balance"] += amount
print(f"✅ Transferred ${amount:.2f}: {from_id} → {to_id}")
# Test cases
test_transfers = [
("ACC001", "ACC002", 500), # ✅ Success
("ACC999", "ACC001", 100), # ❌ AccountNotFoundError
("ACC001", "ACC003", 200), # ❌ AccountLockedError
("ACC002", "ACC001", 5000), # ❌ InsufficientFundsError
("ACC001", "ACC002", 50000), # ❌ DailyLimitError
]
for from_id, to_id, amount in test_transfers:
try:
transfer(from_id, to_id, amount)
except BankError as e:
print(f"❌ {type(e).__name__}: {e}")
Exercise 4: Input Validator
Write a get_valid_input() function that keeps prompting the user until they enter a valid value. Support different types: integer, float, and choice from a list.
Show Solution
def get_valid_input(prompt, input_type="str", choices=None, min_val=None, max_val=None):
"""
Get validated input from the user.
Args:
prompt: Message to display
input_type: "str", "int", or "float"
choices: List of valid choices (for str type)
min_val: Minimum value (for int/float)
max_val: Maximum value (for int/float)
"""
while True:
try:
raw = input(prompt)
if input_type == "int":
value = int(raw)
elif input_type == "float":
value = float(raw)
else:
value = raw.strip()
# Validate choices
if choices and value not in choices:
print(f" Invalid choice. Choose from: {choices}")
continue
# Validate range
if min_val is not None and value < min_val:
print(f" Value must be at least {min_val}")
continue
if max_val is not None and value > max_val:
print(f" Value must be at most {max_val}")
continue
return value
except ValueError:
print(f" Please enter a valid {input_type}")
except KeyboardInterrupt:
print("\n Input cancelled.")
return None
# Test (uncomment to try interactively)
# age = get_valid_input("Enter your age: ", "int", min_val=0, max_val=150)
# print(f"Age: {age}")
# color = get_valid_input("Pick a color: ", "str", choices=["red", "green", "blue"])
# print(f"Color: {color}")
# Example of the function structure for non-interactive testing:
print("get_valid_input() is ready for interactive use!")
print("It handles: ValueError, range validation, choice validation, KeyboardInterrupt")
Exercise 5: Debug This Code
The following code has 5 bugs. Find and fix them all.
def analyze_scores(students):
results = []
for student in students:
name = student["name"]
scores = student["scores"]
average = sum(scores) / len(scores)
if average >= 90:
grade = "A"
elif average >= 80:
grade = "B"
elif average >= 70:
grade = "C"
else:
grade = "F"
results.append({"name": name, "average": average, "grade": grade})
best = max(results, key=lambda r: r["average"])
print(f"Best student: {best['name']} ({best['average']:.1f})")
return results
# Test data
students = [
{"name": "Alice", "scores": [92, 88, 95]},
{"name": "Bob", "scores": []},
{"name": "Carol", "scores": [78, 82, "75"]},
{"name": "Dave"},
{"name": None, "scores": [90, 85]},
]
results = analyze_scores(students)
for r in results:
print(f"{r['name']}: {r['average']:.1f} ({r['grade']})")
Show Solution
Bugs found:
- Bug 1:
Bobhas empty scores →ZeroDivisionErroronlen(scores)= 0 - Bug 2: Carol’s score
"75"is a string →sum()fails withTypeError - Bug 3:
Davehas no"scores"key →KeyError - Bug 4:
Nonename →printworks but is misleading - Bug 5: If
resultsis empty,max()raisesValueError
Fixed version:
def analyze_scores(students):
"""Analyze student scores with proper error handling."""
results = []
for i, student in enumerate(students):
try:
# Bug 4: Handle missing or invalid name
name = student.get("name")
if not name:
print(f" ⚠️ Student #{i+1}: Missing name, skipping")
continue
# Bug 3: Handle missing scores key
if "scores" not in student:
print(f" ⚠️ {name}: No scores data, skipping")
continue
scores = student["scores"]
# Bug 1: Handle empty scores
if not scores:
print(f" ⚠️ {name}: Empty scores list, skipping")
continue
# Bug 2: Ensure all scores are numbers
clean_scores = []
for s in scores:
try:
clean_scores.append(float(s))
except (ValueError, TypeError):
print(f" ⚠️ {name}: Invalid score '{s}', ignoring")
if not clean_scores:
print(f" ⚠️ {name}: No valid scores, skipping")
continue
average = sum(clean_scores) / len(clean_scores)
if average >= 90:
grade = "A"
elif average >= 80:
grade = "B"
elif average >= 70:
grade = "C"
else:
grade = "F"
results.append({"name": name, "average": average, "grade": grade})
except Exception as e:
print(f" ❌ Unexpected error for student #{i+1}: {e}")
# Bug 5: Handle empty results
if results:
best = max(results, key=lambda r: r["average"])
print(f"\nBest student: {best['name']} ({best['average']:.1f})")
else:
print("\nNo valid results to analyze")
return results
# Test
students = [
{"name": "Alice", "scores": [92, 88, 95]},
{"name": "Bob", "scores": []},
{"name": "Carol", "scores": [78, 82, "75"]},
{"name": "Dave"},
{"name": None, "scores": [90, 85]},
]
results = analyze_scores(students)
print()
for r in results:
print(f"{r['name']}: {r['average']:.1f} ({r['grade']})")
Output:
⚠️ Bob: Empty scores list, skipping
⚠️ Dave: No scores data, skipping
⚠️ Student #5: Missing name, skipping
Best student: Alice (91.7)
Alice: 91.7 (A)
Carol: 78.3 (C)
Scoring Guide
| Score | Level |
|---|---|
| 5/5 | Excellent — Exception handling expert |
| 3-4/5 | Good — Solid error handling skills |
| 1-2/5 | Keep practicing — Review try/except and custom exceptions |
Next chapter: Practice & Next Steps →