Week 5: Functions & 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)def 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 prints
Effective refactoring prompts:
"Act as a senior Python developer. Review this script and refactor it into
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 None
BaseException
Exception
(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)finally
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
for file operations__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 closed
def 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"]])
What you’ll accomplish:
# From this (messy, fragile code)
total = 0
for user in users:
if user.get("age"):
total += user["age"]
print("Average:", total/len(users)) # Will crash if no users!
# To this (clean, robust functions)
def calculate_average_age(users: list[dict]) -> float | None:
"""Calculate average age with proper error handling."""
try:
valid_ages = [user["age"] for user in users
if isinstance(user.get("age"), (int, float))]
return sum(valid_ages) / len(valid_ages) if valid_ages else None
except (KeyError, TypeError) as e:
logger.warning(f"Error calculating average age: {e}")
return None
Lab objectives: Clean code organization, defensive programming patterns, professional error handling
Key takeaways for your development journey: - Design functions that do one thing well with clear interfaces - Handle errors proactively rather than reactively - 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 - Write code that your future self (and teammates) will thank you for
IS4010: App Development with AI