Week 05: Functions and error handling
def keyword, a descriptive name, parameters with type hints, a return type, and a docstring (here we’re using a numpy-style docstring)Parameters and Returns sections once the arguments or the return value need explainingdef create_user_profile(name: str, age: int, city: str) -> dict:
"""Create a user profile dictionary."""
return {
"name": name,
"age": age,
"city": city,
"account_created": "today"
}
# Positional arguments (order matters)
profile1 = create_user_profile("Alice", 30, "Cincinnati")
# Keyword arguments (order doesn't matter)
profile2 = create_user_profile(city="Columbus", name="Bob", age=25)None as default, then check and assign actual default in function bodydef send_notification(message: str, urgency: str = "normal",
channels: list = None) -> dict:
"""Send a notification through specified channels."""
if channels is None:
channels = ["email"] # Safe default assignment
return {
"message": message,
"urgency": urgency,
"channels": channels,
"timestamp": "2024-03-15T10:30:00"
}
# Uses defaults
result1 = send_notification("Server backup complete")
# Overrides defaults
result2 = send_notification("Critical error!", "high", ["email", "slack", "phone"])args and kwargs are standard (but you can use other names)def func(required, *args, **kwargs):def analyze_user_data(user_id: int, *metrics: str, **options) -> dict:
"""Analyze user data with flexible metrics and options."""
print(f"Analyzing user {user_id}")
print(f"Metrics to analyze: {metrics}") # Tuple of strings
print(f"Analysis options: {options}") # Dictionary
results = {"user_id": user_id, "metrics_count": len(metrics)}
# Process each metric
for metric in metrics:
results[f"{metric}_score"] = 85 # Simulated analysis
# Apply options
if options.get("detailed", False):
results["detailed_breakdown"] = "Available"
return results
# Flexible function calls
result1 = analyze_user_data(123, "engagement", "retention")
result2 = analyze_user_data(456, "clicks", "views", "shares",
detailed=True, format="json")**dict to pass dictionary as keyword arguments*list to pass list items as positional argumentsdef update_user_settings(user_data: dict, **new_settings) -> dict:
"""Update user settings, returning a new dictionary."""
# Create a copy to avoid modifying original
updated_data = user_data.copy()
updated_data.update(new_settings)
return updated_data
def calculate_statistics(numbers: list[float]) -> dict:
"""Calculate basic statistics from a list of numbers."""
if not numbers:
return {"error": "Empty list provided"}
return {
"count": len(numbers),
"sum": sum(numbers),
"average": sum(numbers) / len(numbers),
"min": min(numbers),
"max": max(numbers)
}
# Example usage
user = {"name": "Alice", "theme": "dark", "notifications": True}
preferences = {"theme": "light", "language": "es"}
# Update settings using dictionary unpacking
updated_user = update_user_settings(user, **preferences)
# Work with lists
scores = [85.5, 92.0, 78.5, 96.0, 89.5]
stats = calculate_statistics(scores)# GOOD: Single responsibility, clear purpose
def calculate_order_total(items: list[dict]) -> float:
"""Calculate the total cost of items in an order."""
return sum(item["price"] * item["quantity"] for item in items)
def apply_discount(total: float, discount_percent: float) -> float:
"""Apply a percentage discount to a total."""
return total * (1 - discount_percent / 100)
def format_currency(amount: float) -> str:
"""Format a number as currency."""
return f"${amount:.2f}"
# BAD: Multiple responsibilities in one function
def process_order_badly(items, discount, tax_rate):
total = 0
for item in items:
total += item["price"] * item["quantity"]
discounted = total * (1 - discount / 100)
with_tax = discounted * (1 + tax_rate / 100)
formatted = f"${with_tax:.2f}"
print(f"Order processed: {formatted}") # Side effect!
return with_tax # Returns number, but also printsEffective refactoring prompts:
"Review this Python script and propose a refactoring plan before editing it.
clean, reusable functions. Each function should:
- Have a single, clear responsibility
- Include type hints and numpy-style docstrings
- Handle edge cases appropriately
- Follow PEP 8 naming conventions"
What to ask for: - Specific function separation suggestions - Error handling improvements - Performance optimization opportunities - Code organization and module structure
# Syntax error - won't even run
# print("Hello world" # Missing closing parenthesis
# Runtime errors - code runs until exception occurs
numbers = [1, 2, 3]
print(numbers[10]) # IndexError: list index out of range
result = 10 / 0 # ZeroDivisionError: division by zero
age = int("not a number") # ValueError: invalid literal for int()try block contains the code that might failexcept block contains the code that runs only if an error occurs in the try blockdef safe_divide(a: float, b: float) -> float | None:
"""Safely divide two numbers, returning None if division fails."""
try:
result = a / b
return result
except ZeroDivisionError:
print(f"Cannot divide {a} by zero")
return None
except TypeError:
print("Both arguments must be numbers")
return None
# Usage examples
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # Cannot divide 10 by zero, returns None
print(safe_divide(10, "x")) # Both arguments must be numbers, returns NoneBaseExceptionException (the ones you typically catch)Exception catches all its subclasses# Common exception types and when they occur
def demonstrate_exceptions():
examples = {
"ValueError": "int('not_a_number')",
"TypeError": "'hello' + 5",
"KeyError": "{'a': 1}['missing_key']",
"IndexError": "[1, 2, 3][10]",
"FileNotFoundError": "open('nonexistent.txt')",
"ZeroDivisionError": "10 / 0",
"AttributeError": "'hello'.nonexistent_method()"
}
for exception_type, code in examples.items():
print(f"{exception_type}: {code}")
# Multiple exception handling
def robust_data_processing(data: dict, key: str) -> str:
try:
value = data[key] # Could raise KeyError
number = int(value) # Could raise ValueError
result = 100 / number # Could raise ZeroDivisionError
return f"Result: {result}"
except KeyError:
return f"Key '{key}' not found in data"
except ValueError:
return f"Value '{data[key]}' is not a valid number"
except ZeroDivisionError:
return "Cannot divide by zero"
except Exception as e: # Catch any other unexpected exceptions
return f"Unexpected error: {e}"Exception or its subclassesclass InsufficientFundsError(Exception):
"""Raised when account doesn't have enough money for transaction."""
def __init__(self, balance: float, amount: float):
self.balance = balance
self.amount = amount
self.shortfall = amount - balance
super().__init__(f"Insufficient funds: need ${amount}, have ${balance}")
class InvalidUserAgeError(Exception):
"""Raised when user age is outside valid range."""
pass
def withdraw_money(balance: float, amount: float) -> float:
"""Withdraw money from account, raising custom exception if insufficient funds."""
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
def validate_user_age(age: int) -> bool:
"""Validate user age, raising custom exception if invalid."""
if not (13 <= age <= 120):
raise InvalidUserAgeError(f"Age {age} is not valid (must be 13-120)")
return True
# Usage with custom exception handling
try:
new_balance = withdraw_money(50.0, 75.0)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")
print(f"You need ${e.shortfall:.2f} more")try block completes successfully (no exception was raised)try block succeeds or raises, making it useful for cleanupfinally for resource cleanup (files, network connections, locks)finally ensures cleanup even if exceptions are re-raiseddef process_user_data(filename: str) -> dict:
"""Process user data from file with proper cleanup."""
file_handle = None
try:
file_handle = open(filename, 'r')
data = file_handle.read()
parsed_data = eval(data) # Dangerous! Just for demonstration
return {"status": "success", "data": parsed_data}
except FileNotFoundError:
return {"status": "error", "message": f"File {filename} not found"}
except SyntaxError:
return {"status": "error", "message": "Invalid data format in file"}
else:
print("File processed successfully - no exceptions occurred")
return {"status": "success", "message": "Data processed"}
finally:
# This ALWAYS runs - exception or not
if file_handle and not file_handle.closed:
file_handle.close()
print("File closed in finally block")
# Demonstrate all clauses
result = process_user_data("user_data.txt")
print(f"Result: {result}")with statement: Guarantees cleanup even if exceptions occurwith so files close when the block exits__enter__ and __exit__ methods# OLD WAY: Manual file handling (error-prone)
def read_config_old(filename: str) -> dict:
try:
file = open(filename, 'r')
content = file.read()
return {"content": content}
except FileNotFoundError:
return {"error": "File not found"}
finally:
if 'file' in locals() and not file.closed:
file.close()
# NEW WAY: Context manager (automatic cleanup)
def read_config_new(filename: str) -> dict:
try:
with open(filename, 'r') as file:
content = file.read()
return {"content": content}
except FileNotFoundError:
return {"error": "File not found"}
# File automatically closed here, even if exception occurs!
# Multiple files with context managers
def compare_files(file1: str, file2: str) -> bool:
try:
with open(file1, 'r') as f1, open(file2, 'r') as f2:
return f1.read() == f2.read()
except FileNotFoundError:
return False
# Both files automatically closeddef create_user_account(username: str, age: int, email: str) -> dict:
"""Create user account with comprehensive input validation."""
# Input validation with clear error messages
if not isinstance(username, str):
raise TypeError("Username must be a string")
if not username or len(username.strip()) == 0:
raise ValueError("Username cannot be empty or only whitespace")
if len(username) < 3:
raise ValueError("Username must be at least 3 characters long")
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if not (13 <= age <= 120):
raise ValueError("Age must be between 13 and 120")
if not isinstance(email, str) or '@' not in email:
raise ValueError("Email must be a valid email address")
# Only process if all validation passes
return {
"username": username.strip().lower(),
"age": age,
"email": email.lower(),
"account_id": f"user_{username}_{age}",
"status": "active"
}
# Safe usage with validation
try:
user = create_user_account("Alice123", 25, "alice@example.com")
print(f"Created user: {user}")
except (TypeError, ValueError) as e:
print(f"Invalid input: {e}")# LBYL Approach: Check first, then act
def get_user_score_lbyl(users: dict, user_id: str) -> float:
"""Get user score using Look Before You Leap pattern."""
if user_id not in users:
return 0.0
user = users[user_id]
if 'scores' not in user:
return 0.0
if len(user['scores']) == 0:
return 0.0
return sum(user['scores']) / len(user['scores'])
# EAFP Approach: Try it and handle exceptions
def get_user_score_eafp(users: dict, user_id: str) -> float:
"""Get user score using Easier to Ask Forgiveness pattern."""
try:
scores = users[user_id]['scores']
return sum(scores) / len(scores)
except KeyError:
return 0.0 # User or scores key doesn't exist
except ZeroDivisionError:
return 0.0 # Empty scores list
# When to use each approach
def demonstrate_patterns():
users = {
"alice": {"scores": [85, 92, 78]},
"bob": {"scores": []},
"charlie": {}
}
# Both approaches handle the same edge cases
print("LBYL results:", [get_user_score_lbyl(users, uid)
for uid in ["alice", "bob", "charlie", "diana"]])
print("EAFP results:", [get_user_score_eafp(users, uid)
for uid in ["alice", "bob", "charlie", "diana"]])calculate_average_age(users) and return 0.0 when no ages are validget_active_user_emails(users) and return an empty list when none qualifyImplementation scaffold:
def calculate_average_age(users: list[dict]) -> float:
"""Return the average numeric age, or 0.0 if none are valid."""
# TODO: collect the valid ages and calculate the specified result
...
def get_active_user_emails(users: list[dict]) -> list[str]:
"""Return emails for users whose is_active value is truthy."""
# TODO: preserve input order and skip missing or invalid records
...Instructions: week05/lab05.md
Key takeaways: - Design functions that do one thing well with clear interfaces - Handle expected failures at the layer that can respond meaningfully - Validate inputs to prevent problems before they occur - Use context managers for automatic resource management - Choose EAFP vs LBYL based on your specific use case - Verify behavior with focused tests for success and failure paths
IS 4010: App Dev with AI