Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on handling multiple exceptions in Python! ๐ Have you ever wondered how to gracefully handle different types of errors in your programs? Today, weโll explore the art of catching and managing multiple exceptions like a pro!
Youโll discover how handling multiple exceptions can make your Python applications more robust and user-friendly. Whether youโre building web applications ๐, processing data ๐, or creating automation scripts ๐ค, understanding multiple exception handling is essential for writing bulletproof code.
By the end of this tutorial, youโll feel confident handling any combination of errors that come your way! Letโs dive in! ๐โโ๏ธ
๐ Understanding Multiple Exceptions
๐ค What are Multiple Exceptions?
Handling multiple exceptions is like being a skilled juggler ๐คน - you need to catch different types of balls (errors) and handle each one appropriately. Think of it as having different safety nets for different circus acts!
In Python terms, multiple exception handling allows you to catch and respond to different error types in a single try block. This means you can:
- โจ Handle different errors with specific responses
- ๐ Keep your code running smoothly
- ๐ก๏ธ Provide meaningful feedback to users
๐ก Why Handle Multiple Exceptions?
Hereโs why developers love multiple exception handling:
- Specific Error Handling ๐ฏ: Respond appropriately to each error type
- Better User Experience ๐ป: Provide helpful error messages
- Code Resilience ๐ก๏ธ: Prevent crashes from unexpected errors
- Cleaner Code ๐งน: Organize error handling logically
Real-world example: Imagine building a file processor ๐. You might encounter file not found errors, permission errors, or disk full errors - each needing different handling!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, Multiple Exceptions!
def divide_numbers(a, b):
try:
# ๐ฏ The risky operation
result = a / b
print(f"Result: {result} โจ")
return result
except ZeroDivisionError:
# ๐ซ Handle division by zero
print("Oops! Can't divide by zero! ๐คฏ")
return None
except TypeError:
# โ Handle wrong data types
print("Please provide numbers only! ๐ข")
return None
# ๐ฎ Let's test it!
divide_numbers(10, 2) # Works great!
divide_numbers(10, 0) # Catches zero division
divide_numbers(10, "2") # Catches type error
๐ก Explanation: Notice how we handle each exception type separately! This gives us precise control over error responses.
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Multiple except blocks
def process_user_input(data):
try:
# ๐จ Process the data
value = int(data)
result = 100 / value
return f"Success! Result is {result} ๐"
except ValueError:
# ๐ Handle conversion errors
return "Please enter a valid number! ๐ข"
except ZeroDivisionError:
# ๐ซ Handle division by zero
return "Zero is not allowed! ๐ซ"
except Exception as e:
# ๐ก๏ธ Catch-all for unexpected errors
return f"Something went wrong: {e} ๐
"
# ๐จ Pattern 2: Catching multiple exceptions together
def read_config_file(filename):
try:
# ๐ Try to read the file
with open(filename, 'r') as f:
return f.read()
except (FileNotFoundError, PermissionError) as e:
# ๐ Handle file-related errors together
print(f"File error: {e} ๐")
return None
# ๐ Pattern 3: Exception hierarchy
def safe_operation(data):
try:
# ๐ฏ Some risky operation
process_data(data)
except KeyboardInterrupt:
# โจ๏ธ Always catch this first!
print("Operation cancelled by user! ๐")
raise # Re-raise to allow proper cleanup
except Exception as e:
# ๐ก๏ธ General exception handler
print(f"Error occurred: {e} ๐")
๐ก Practical Examples
๐ Example 1: Online Shopping Cart
Letโs build something real:
# ๐๏ธ Define our shopping cart with error handling
class ShoppingCart:
def __init__(self):
self.items = {} # ๐ Our cart storage
self.max_items = 10 # ๐ฆ Cart limit
def add_item(self, item_name, price, quantity):
"""Add item to cart with multiple error checks! ๐ฏ"""
try:
# ๐ Validate inputs
if not isinstance(item_name, str):
raise TypeError("Item name must be text! ๐")
if price <= 0:
raise ValueError("Price must be positive! ๐ฐ")
if quantity <= 0:
raise ValueError("Quantity must be at least 1! ๐ข")
if len(self.items) >= self.max_items:
raise OverflowError("Cart is full! ๐ฆ")
# โจ Add to cart
self.items[item_name] = {
'price': float(price),
'quantity': int(quantity),
'emoji': self._get_item_emoji(item_name)
}
print(f"Added {quantity}x {item_name} to cart! {self._get_item_emoji(item_name)}")
except TypeError as e:
print(f"โ Type Error: {e}")
except ValueError as e:
print(f"โ ๏ธ Value Error: {e}")
except OverflowError as e:
print(f"๐ซ Cart Error: {e}")
except Exception as e:
print(f"๐ฑ Unexpected error: {e}")
def _get_item_emoji(self, item_name):
"""Get emoji for items! ๐จ"""
emojis = {
'apple': '๐', 'banana': '๐', 'coffee': 'โ',
'pizza': '๐', 'book': '๐', 'laptop': '๐ป'
}
return emojis.get(item_name.lower(), '๐ฆ')
def checkout(self):
"""Calculate total with error handling! ๐ฐ"""
try:
if not self.items:
raise ValueError("Cart is empty! ๐")
total = sum(item['price'] * item['quantity']
for item in self.items.values())
print(f"\n๐งพ Your cart total: ${total:.2f}")
return total
except ValueError as e:
print(f"โ ๏ธ Checkout Error: {e}")
return 0
except Exception as e:
print(f"๐ฅ Checkout failed: {e}")
return 0
# ๐ฎ Let's use our cart!
cart = ShoppingCart()
# โ
Valid additions
cart.add_item("Apple", 1.99, 3)
cart.add_item("Coffee", 4.99, 1)
# โ Invalid additions (will be caught!)
cart.add_item(123, 2.99, 1) # Wrong type
cart.add_item("Banana", -1.00, 2) # Negative price
cart.add_item("Pizza", 9.99, 0) # Zero quantity
# ๐ฐ Checkout
cart.checkout()
๐ฏ Try it yourself: Add a remove_item
method with its own exception handling!
๐ฎ Example 2: Game Save System
Letโs make it fun with a game save system:
import json
import os
# ๐ Game save system with robust error handling
class GameSaveManager:
def __init__(self, save_directory="game_saves"):
self.save_dir = save_directory
self._ensure_save_directory()
def _ensure_save_directory(self):
"""Create save directory if needed! ๐"""
try:
os.makedirs(self.save_dir, exist_ok=True)
except PermissionError:
print("โ ๏ธ No permission to create save directory!")
except Exception as e:
print(f"๐ Directory error: {e}")
def save_game(self, player_name, game_data):
"""Save game with multiple error checks! ๐พ"""
try:
# ๐ Validate player name
if not player_name or not isinstance(player_name, str):
raise ValueError("Invalid player name! ๐ค")
# ๐ฎ Validate game data
if not isinstance(game_data, dict):
raise TypeError("Game data must be a dictionary! ๐")
# ๐ Add metadata
save_data = {
'player': player_name,
'level': game_data.get('level', 1),
'score': game_data.get('score', 0),
'achievements': game_data.get('achievements', []),
'emoji': '๐ฎ'
}
# ๐พ Save to file
filename = os.path.join(self.save_dir, f"{player_name}_save.json")
with open(filename, 'w') as f:
json.dump(save_data, f, indent=2)
print(f"โ
Game saved for {player_name}! ๐ฎ")
return True
except ValueError as e:
print(f"โ Invalid input: {e}")
except TypeError as e:
print(f"โ Type error: {e}")
except PermissionError:
print("๐ซ No permission to save file!")
except IOError:
print("๐พ Disk error - could not save!")
except Exception as e:
print(f"๐ฑ Unexpected save error: {e}")
return False
def load_game(self, player_name):
"""Load game with error recovery! ๐"""
try:
filename = os.path.join(self.save_dir, f"{player_name}_save.json")
# ๐ Try to load the save
with open(filename, 'r') as f:
save_data = json.load(f)
print(f"โ
Loaded save for {player_name}!")
print(f"๐ Level: {save_data['level']}, Score: {save_data['score']}")
return save_data
except FileNotFoundError:
print(f"๐ No save found for {player_name}")
return self._create_new_save(player_name)
except json.JSONDecodeError:
print("๐ฅ Save file corrupted! Creating backup...")
self._backup_corrupted_save(filename)
return self._create_new_save(player_name)
except PermissionError:
print("๐ซ No permission to read save file!")
except Exception as e:
print(f"๐ฑ Load error: {e}")
return None
def _create_new_save(self, player_name):
"""Create a fresh save! ๐"""
return {
'player': player_name,
'level': 1,
'score': 0,
'achievements': ['๐ First Steps'],
'emoji': '๐ฎ'
}
def _backup_corrupted_save(self, filename):
"""Backup corrupted saves! ๐ก๏ธ"""
try:
backup_name = f"{filename}.corrupted"
os.rename(filename, backup_name)
print(f"๐ฆ Corrupted save backed up to {backup_name}")
except:
pass # Silently fail backup
# ๐ฎ Let's test our save system!
save_manager = GameSaveManager()
# โ
Valid saves
save_manager.save_game("Alice", {'level': 5, 'score': 1000})
save_manager.save_game("Bob", {'level': 3, 'score': 500, 'achievements': ['๐ First Boss']})
# โ Invalid saves (handled gracefully!)
save_manager.save_game("", {'level': 1}) # Empty name
save_manager.save_game("Charlie", "not a dict") # Wrong type
# ๐ Load games
save_manager.load_game("Alice") # Existing save
save_manager.load_game("NewPlayer") # Non-existent save
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Custom Exception Classes
When youโre ready to level up, create your own exceptions:
# ๐ฏ Custom exceptions for a banking app
class BankingError(Exception):
"""Base exception for banking operations ๐ฆ"""
pass
class InsufficientFundsError(BankingError):
"""Not enough money! ๐ธ"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Cannot withdraw ${amount} from balance ${balance}")
class AccountLockedError(BankingError):
"""Account is locked! ๐"""
pass
class InvalidTransactionError(BankingError):
"""Invalid transaction! โ"""
pass
# ๐ฆ Banking system using custom exceptions
class BankAccount:
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self.balance = initial_balance
self.locked = False
self.transactions = []
def withdraw(self, amount):
"""Withdraw with custom exception handling! ๐ฐ"""
try:
# ๐ Check if locked
if self.locked:
raise AccountLockedError("Account is locked! ๐")
# ๐ต Check amount validity
if amount <= 0:
raise InvalidTransactionError("Amount must be positive! โ")
# ๐ธ Check sufficient funds
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
# โ
Process withdrawal
self.balance -= amount
self.transactions.append(f"Withdrew ${amount} ๐ธ")
print(f"โ
Withdrew ${amount}. New balance: ${self.balance}")
except AccountLockedError:
print("๐ Cannot withdraw - account is locked!")
except InsufficientFundsError as e:
print(f"๐ธ Insufficient funds! You have ${e.balance} but need ${e.amount}")
except InvalidTransactionError as e:
print(f"โ Invalid transaction: {e}")
except Exception as e:
print(f"๐ฑ Unexpected error: {e}")
# ๐ฎ Test our banking system!
account = BankAccount("123456", 100)
account.withdraw(50) # โ
Works!
account.withdraw(200) # ๐ธ Insufficient funds!
account.withdraw(-10) # โ Invalid amount!
account.locked = True
account.withdraw(10) # ๐ Account locked!
๐๏ธ Advanced Topic 2: Exception Chaining and Context
For the brave developers, letโs explore exception chaining:
# ๐ Advanced exception chaining
class DataProcessor:
def __init__(self):
self.data = []
self.processed = []
def load_data(self, source):
"""Load data with exception chaining! ๐"""
try:
if source == "database":
# ๐๏ธ Simulate database loading
raise ConnectionError("Database unavailable! ๐")
elif source == "file":
# ๐ Simulate file loading
with open("data.txt", 'r') as f:
self.data = f.readlines()
else:
raise ValueError(f"Unknown source: {source}")
except FileNotFoundError as e:
# ๐ Chain exceptions for context
raise RuntimeError("Data loading failed! ๐") from e
except ConnectionError as e:
# ๐ Provide alternative suggestion
print(f"โ ๏ธ {e} - Trying backup source...")
try:
self.load_data("file") # Try alternative
except Exception as backup_error:
raise RuntimeError("All data sources failed! ๐ฑ") from backup_error
def process_data(self):
"""Process with context managers! ๐จ"""
class ProcessingContext:
def __enter__(self):
print("๐ฌ Starting processing...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
print("โ
Processing completed successfully!")
else:
print(f"โ Processing failed: {exc_val}")
# Return False to propagate the exception
return False
try:
with ProcessingContext():
if not self.data:
raise ValueError("No data to process! ๐")
# ๐จ Process each item
for item in self.data:
if "error" in item.lower():
raise RuntimeError(f"Found error in data: {item.strip()}")
self.processed.append(item.upper())
except ValueError as e:
print(f"โ ๏ธ Data validation error: {e}")
except RuntimeError as e:
print(f"๐ฅ Processing error: {e}")
except Exception as e:
print(f"๐ฑ Unexpected error: {e}")
# ๐ฎ Test advanced features!
processor = DataProcessor()
# Try different scenarios
try:
processor.load_data("database") # Will fail and try backup
except RuntimeError as e:
print(f"Final error: {e}")
print(f"Original cause: {e.__cause__}")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Catching Exceptions Too Broadly
# โ Wrong way - catches everything!
try:
result = risky_operation()
except: # ๐ฐ This catches EVERYTHING including KeyboardInterrupt!
print("Something went wrong")
# โ
Correct way - be specific!
try:
result = risky_operation()
except ValueError:
print("Invalid value provided! ๐")
except IOError:
print("File operation failed! ๐")
except Exception as e: # ๐ก๏ธ Catch other exceptions but not system exits
print(f"Unexpected error: {e}")
๐คฏ Pitfall 2: Wrong Exception Order
# โ Dangerous - specific exceptions never caught!
try:
process_file("data.txt")
except Exception: # ๐ฑ This catches everything first!
print("General error")
except FileNotFoundError: # ๐ฅ Never reached!
print("File not found")
# โ
Safe - specific exceptions first!
try:
process_file("data.txt")
except FileNotFoundError: # ๐ฏ Specific first
print("File not found! ๐")
except PermissionError: # ๐ Then other specific
print("No permission! ๐ซ")
except Exception as e: # ๐ก๏ธ General last
print(f"Other error: {e}")
๐ ๏ธ Best Practices
- ๐ฏ Be Specific: Catch specific exceptions, not bare
except:
- ๐ Order Matters: Put specific exceptions before general ones
- ๐ก๏ธ Always Have a Safety Net: Use
Exception
as last resort - ๐จ Create Custom Exceptions: For domain-specific errors
- โจ Provide Context: Give helpful error messages to users
๐งช Hands-On Exercise
๐ฏ Challenge: Build a File Processing System
Create a robust file processor that handles multiple error scenarios:
๐ Requirements:
- โ Read files with various formats (txt, json, csv)
- ๐ท๏ธ Handle file not found, permission, and format errors
- ๐ค Parse and validate data from files
- ๐ Generate error reports for failures
- ๐จ Each operation needs specific error handling!
๐ Bonus Points:
- Add retry logic for temporary failures
- Create custom exceptions for validation errors
- Implement logging for all errors
๐ก Solution
๐ Click to see solution
import json
import csv
import time
from datetime import datetime
# ๐ฏ Custom exceptions for our file processor
class FileProcessingError(Exception):
"""Base exception for file processing ๐"""
pass
class ValidationError(FileProcessingError):
"""Data validation failed! โ"""
pass
class UnsupportedFormatError(FileProcessingError):
"""File format not supported! ๐"""
pass
# ๐ Our robust file processor!
class FileProcessor:
def __init__(self):
self.supported_formats = ['.txt', '.json', '.csv']
self.processed_files = []
self.error_log = []
def process_file(self, filename, retry_count=3):
"""Process file with multiple error handlers! ๐ฏ"""
for attempt in range(retry_count):
try:
# ๐ Check file extension
if not any(filename.endswith(fmt) for fmt in self.supported_formats):
raise UnsupportedFormatError(f"Format not supported: {filename}")
# ๐ Read and process based on type
if filename.endswith('.json'):
data = self._process_json(filename)
elif filename.endswith('.csv'):
data = self._process_csv(filename)
else:
data = self._process_text(filename)
# โ
Validate data
self._validate_data(data, filename)
# ๐ Success!
self.processed_files.append({
'filename': filename,
'records': len(data) if isinstance(data, list) else 1,
'timestamp': datetime.now().isoformat(),
'status': 'โ
'
})
print(f"โ
Successfully processed {filename}!")
return data
except FileNotFoundError:
self._log_error(filename, "File not found! ๐")
print(f"โ File not found: {filename}")
break # Don't retry for missing files
except PermissionError:
self._log_error(filename, "Permission denied! ๐")
print(f"๐ซ No permission to read: {filename}")
break # Don't retry for permission issues
except json.JSONDecodeError as e:
self._log_error(filename, f"Invalid JSON: {e} ๐")
print(f"โ JSON parsing error in {filename}")
break # Don't retry for format errors
except UnsupportedFormatError as e:
self._log_error(filename, str(e))
print(f"๐ {e}")
break
except ValidationError as e:
self._log_error(filename, f"Validation failed: {e} โ ๏ธ")
print(f"โ ๏ธ Validation error in {filename}: {e}")
break
except IOError as e:
self._log_error(filename, f"IO Error: {e} ๐พ")
if attempt < retry_count - 1:
print(f"๐พ IO Error, retrying in 1 second... (Attempt {attempt + 1}/{retry_count})")
time.sleep(1)
else:
print(f"๐ฅ Failed after {retry_count} attempts!")
except Exception as e:
self._log_error(filename, f"Unexpected: {e} ๐ฑ")
print(f"๐ฑ Unexpected error processing {filename}: {e}")
break
return None
def _process_json(self, filename):
"""Process JSON files! ๐"""
with open(filename, 'r') as f:
return json.load(f)
def _process_csv(self, filename):
"""Process CSV files! ๐"""
data = []
with open(filename, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
data.append(row)
return data
def _process_text(self, filename):
"""Process text files! ๐"""
with open(filename, 'r') as f:
return f.readlines()
def _validate_data(self, data, filename):
"""Validate processed data! ๐"""
if not data:
raise ValidationError("File is empty!")
if isinstance(data, list) and len(data) > 1000:
raise ValidationError("File too large (>1000 records)!")
def _log_error(self, filename, error_msg):
"""Log errors for reporting! ๐"""
self.error_log.append({
'filename': filename,
'error': error_msg,
'timestamp': datetime.now().isoformat()
})
def generate_report(self):
"""Generate processing report! ๐"""
print("\n๐ File Processing Report")
print("=" * 40)
print(f"\nโ
Successfully processed: {len(self.processed_files)} files")
for file_info in self.processed_files:
print(f" ๐ {file_info['filename']} ({file_info['records']} records)")
print(f"\nโ Failed to process: {len(self.error_log)} files")
for error_info in self.error_log:
print(f" โ ๏ธ {error_info['filename']}: {error_info['error']}")
# ๐ฎ Test our file processor!
processor = FileProcessor()
# Test various scenarios
test_files = [
"data.json", # May or may not exist
"config.txt", # May or may not exist
"records.csv", # May or may not exist
"image.png", # Unsupported format
"protected.txt" # May have permission issues
]
for filename in test_files:
processor.process_file(filename)
# ๐ Generate final report
processor.generate_report()
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Handle multiple exceptions with confidence ๐ช
- โ Create specific error responses for different scenarios ๐ฏ
- โ Build robust applications that donโt crash easily ๐ก๏ธ
- โ Debug issues by understanding error types ๐
- โ Write professional code with proper error handling! ๐
Remember: Good error handling is like wearing a seatbelt - you hope you donโt need it, but youโll be glad itโs there! ๐
๐ค Next Steps
Congratulations! ๐ Youโve mastered handling multiple exceptions in Python!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Add robust error handling to your existing projects
- ๐ Move on to our next tutorial: Exception Chaining and Context
- ๐ Share your error-handling victories with others!
Remember: Every Python expert has encountered countless errors. The difference is they learned to handle them gracefully! Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ