Python Topic
Python Exception Handling
1. What are Exceptions?
Exceptions are events that occur during program execution that disrupt the normal flow of instructions.
Common Built-in Exceptions:
| Exception | Description | Common Cause |
|---|---|---|
ZeroDivisionError |
Division by zero | 10 / 0 |
FileNotFoundError |
File doesn't exist | open("nonexistent.txt") |
ValueError |
Invalid value conversion | int("abc") |
TypeError |
Operation on wrong type | "hello" + 5 |
IndexError |
Index out of range | list[10] on a 3-item list |
KeyError |
Dictionary key not found | dict["missing_key"] |
NameError |
Variable not defined | print(undefined_var) |
2. Basic Exception Handling: try and except
The Basic Structure:
python
try:
# Code that might cause an exception
risky_operation()
except ExceptionType:
# Code to handle the exception
handle_error()
Simple Examples:
python
# Handling division by zero
try:
result = 10 / 0
print(f"Result: {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
# Handling file operations
try:
with open("nonexistent_file.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("Error: File not found!")
# Handling multiple specific exceptions
try:
number = int(input("Enter a number: "))
result = 100 / number
print(f"100 / {number} = {result}")
except ValueError:
print("Error: Please enter a valid number!")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
3. Advanced Exception Handling
Catching Multiple Exceptions:
python
# Multiple exceptions in one except block
try:
num = int(input("Enter a number: "))
data = [1, 2, 3]
print(data[num])
except (ValueError, IndexError) as e:
print(f"Error: {e}")
print("Please enter a valid index (0-2)")
# Separate except blocks for different handling
try:
file = open("data.txt", "r")
number = int(file.read())
result = 100 / number
except FileNotFoundError:
print("File not found. Using default value.")
result = 0
except ValueError:
print("Invalid data in file.")
result = 0
except ZeroDivisionError:
print("Cannot divide by zero.")
result = 0
Getting Exception Information:
python
try:
number = int("abc")
except ValueError as e:
print(f"Error type: {type(e).__name__}")
print(f"Error message: {e}")
print(f"Full exception info: {repr(e)}")
The else Clause:
python
# else block runs only if no exception occurred
try:
number = int(input("Enter a number: "))
result = 100 / number
except ValueError:
print("Invalid input! Please enter a number.")
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print(f"Operation successful! Result: {result}")
# This only runs if no exceptions were raised
The finally Clause:
python
# finally block always runs, regardless of exceptions
try:
file = open("data.txt", "r")
content = file.read()
number = int(content)
result = 100 / number
except (FileNotFoundError, ValueError, ZeroDivisionError) as e:
print(f"Error: {e}")
result = None
finally:
# This always executes - perfect for cleanup
try:
file.close()
print("File closed successfully.")
except:
pass # file might not have been opened
print("Cleanup completed.")
4. Raising Exceptions
Purpose: You can deliberately raise exceptions when something goes wrong in your code.
Basic Raising:
python
def calculate_square_root(number):
if number < 0:
raise ValueError("Cannot calculate square root of negative number")
return number ** 0.5
try:
result = calculate_square_root(-4)
except ValueError as e:
print(f"Error: {e}") # Error: Cannot calculate square root of negative number
Re-raising Exceptions:
python
def process_data(data):
try:
# Some processing that might fail
if not data:
raise ValueError("Data cannot be empty")
return data.upper()
except Exception as e:
print(f"Error in process_data: {e}")
raise # Re-raise the same exception
try:
result = process_data("")
except ValueError as e:
print(f"Caught error: {e}")
Creating Custom Exceptions:
python
# Custom exception class
class InsufficientFundsError(Exception):
"""Exception raised when account has insufficient funds."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.message = f"Insufficient funds: ${balance} available, but ${amount} requested"
super().__init__(self.message)
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return amount
# Using custom exception
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(f"Withdrawal failed: {e}")
print(f"Available: ${e.balance}, Requested: ${e.amount}")
5. Practical Examples
Example 1: Robust User Input Handling
python
def get_positive_number(prompt):
"""Get a positive number from user with validation."""
while True:
try:
number = float(input(prompt))
if number <= 0:
raise ValueError("Number must be positive")
return number
except ValueError as e:
print(f"Invalid input: {e}")
print("Please try again.\n")
def get_age():
"""Get a valid age from user."""
while True:
try:
age = int(input("Enter your age: "))
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Please enter a realistic age")
return age
except ValueError as e:
print(f"Error: {e}")
print("Please enter a valid age (0-150)\n")
# Usage
try:
age = get_age()
weight = get_positive_number("Enter your weight (kg): ")
height = get_positive_number("Enter your height (m): ")
bmi = weight / (height ** 2)
print(f"Your BMI: {bmi:.2f}")
except KeyboardInterrupt:
print("\nProgram interrupted by user")
except Exception as e:
print(f"Unexpected error: {e}")
Example 2: File Processing with Comprehensive Error Handling
python
import os
from datetime import datetime
def process_data_file(filename):
"""Process a data file with comprehensive error handling."""
results = []
try:
# Check if file exists and is readable
if not os.path.exists(filename):
raise FileNotFoundError(f"File '{filename}' does not exist")
if not os.access(filename, os.R_OK):
raise PermissionError(f"No read permission for '{filename}'")
# Open and process file
with open(filename, 'r') as file:
print(f"Processing file: {filename}")
for line_num, line in enumerate(file, 1):
try:
# Remove whitespace and skip empty lines
line = line.strip()
if not line or line.startswith('#'):
continue
# Process data (example: convert to numbers and square them)
number = float(line)
squared = number ** 2
results.append(squared)
print(f"Line {line_num}: {number} -> {squared}")
except ValueError as e:
print(f"Warning: Invalid data on line {line_num}: '{line}'")
continue # Skip bad lines but continue processing
except FileNotFoundError as e:
print(f"Error: {e}")
return None
except PermissionError as e:
print(f"Error: {e}")
return None
except Exception as e:
print(f"Unexpected error processing file: {e}")
return None
else:
print(f"File processed successfully. {len(results)} valid entries found.")
return results
finally:
print("File processing attempt completed.\n")
# Test the function
files_to_process = ["data.txt", "nonexistent.txt", "protected_file.txt"]
for file in files_to_process:
results = process_data_file(file)
if results:
print(f"Results: {results}\n")
Example 3: Database Connection Simulator
python
import random
import time
class DatabaseConnectionError(Exception):
"""Custom exception for database connection issues."""
pass
class DatabaseTimeoutError(Exception):
"""Custom exception for database timeouts."""
pass
class DatabaseSimulator:
def __init__(self):
self.connected = False
def connect(self):
"""Simulate database connection with random failures."""
print("Attempting to connect to database...")
time.sleep(1) # Simulate connection delay
# Randomly simulate different scenarios
scenario = random.choice(["success", "connection_error", "timeout", "auth_error"])
if scenario == "success":
self.connected = True
print("Database connected successfully!")
return True
elif scenario == "connection_error":
raise DatabaseConnectionError("Cannot connect to database server")
elif scenario == "timeout":
raise DatabaseTimeoutError("Database connection timeout")
elif scenario == "auth_error":
raise PermissionError("Authentication failed")
def execute_query(self, query):
"""Execute a database query."""
if not self.connected:
raise DatabaseConnectionError("Not connected to database")
print(f"Executing query: {query}")
time.sleep(0.5)
# Simulate occasional query errors
if random.random() < 0.3: # 30% chance of error
raise ValueError("Query execution error: Invalid syntax")
return f"Results for: {query}"
def disconnect(self):
"""Disconnect from database."""
if self.connected:
print("Disconnecting from database...")
self.connected = False
print("Database disconnected.")
def run_database_operations():
"""Run database operations with comprehensive error handling."""
db = DatabaseSimulator()
try:
# Attempt to connect with retry logic
max_retries = 3
for attempt in range(max_retries):
try:
db.connect()
break # Connection successful, break out of retry loop
except (DatabaseConnectionError, DatabaseTimeoutError) as e:
print(f"Connection attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
print("Retrying in 2 seconds...")
time.sleep(2)
else:
print("Max retries reached. Giving up.")
return
except PermissionError as e:
print(f"Authentication failed: {e}")
return # Auth errors usually don't benefit from retries
# Execute queries
queries = ["SELECT * FROM users", "UPDATE settings SET value='new'", "INVALID QUERY"]
for query in queries:
try:
result = db.execute_query(query)
print(f"Query result: {result}")
except ValueError as e:
print(f"Query failed: {e}")
# Continue with next query instead of stopping entirely
continue
except Exception as e:
print(f"Unexpected error: {e}")
finally:
# Always disconnect, whether operations succeeded or failed
db.disconnect()
# Run multiple simulations to see different scenarios
print("=== Database Operations Simulation ===\n")
for i in range(3):
print(f"--- Simulation {i + 1} ---")
run_database_operations()
print()
Example 4: Configuration Manager with Validation
python
import json
import os
class ConfigValidationError(Exception):
"""Custom exception for configuration validation errors."""
pass
class ConfigManager:
def __init__(self, config_file="config.json"):
self.config_file = config_file
self.config = {}
def load_config(self):
"""Load configuration from file with error handling."""
try:
if not os.path.exists(self.config_file):
raise FileNotFoundError(f"Config file '{self.config_file}' not found")
with open(self.config_file, 'r') as file:
self.config = json.load(file)
self._validate_config()
print("✅ Configuration loaded successfully")
except json.JSONDecodeError as e:
raise ConfigValidationError(f"Invalid JSON in config file: {e}")
except Exception as e:
raise ConfigValidationError(f"Failed to load config: {e}")
def _validate_config(self):
"""Validate configuration data."""
required_fields = ['database', 'api_key', 'timeout']
for field in required_fields:
if field not in self.config:
raise ConfigValidationError(f"Missing required field: '{field}'")
# Validate specific fields
if not isinstance(self.config['timeout'], (int, float)) or self.config['timeout'] <= 0:
raise ConfigValidationError("Timeout must be a positive number")
if not self.config['api_key'] or not isinstance(self.config['api_key'], str):
raise ConfigValidationError("API key must be a non-empty string")
def get_setting(self, key, default=None):
"""Get a configuration setting safely."""
try:
return self.config[key]
except KeyError:
if default is not None:
return default
raise ConfigValidationError(f"Setting '{key}' not found in config")
# Usage example
def initialize_application():
"""Initialize application with configuration."""
config_manager = ConfigManager("app_config.json")
try:
config_manager.load_config()
# Use configuration
database = config_manager.get_setting('database')
api_key = config_manager.get_setting('api_key')
timeout = config_manager.get_setting('timeout')
print(f"Database: {database}")
print(f"API Key: {'*' * len(api_key)}") # Mask API key
print(f"Timeout: {timeout}s")
return True
except ConfigValidationError as e:
print(f"❌ Configuration error: {e}")
print("Please check your configuration file.")
return False
except Exception as e:
print(f"❌ Unexpected error: {e}")
return False
# Test with different scenarios
print("=== Configuration Manager Test ===")
initialize_application()
6. Best Practices
1. Be Specific with Exceptions:
python
# Good - specific exceptions
try:
value = int(user_input)
except ValueError:
print("Please enter a valid number")
# Avoid - too broad
try:
value = int(user_input)
except: # Catches ALL exceptions - dangerous!
print("Something went wrong")
2. Don't Use Exceptions for Normal Flow:
python
# Bad - using exceptions for normal program flow
try:
while True:
item = next(iterator)
process(item)
except StopIteration:
pass # This is the normal way loops end!
# Good - explicit check
for item in iterator:
process(item)
3. Provide Helpful Error Messages:
python
try:
config = load_config_file("settings.json")
except FileNotFoundError:
print("Error: Configuration file 'settings.json' not found.")
print("Please create the file or check the path.")
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in configuration file: {e}")
print("Please validate your JSON syntax.")
4. Use Finally for Cleanup:
python
def process_file(filename):
file = None
try:
file = open(filename, 'r')
# Process file
return process_content(file.read())
except IOError as e:
print(f"File error: {e}")
return None
finally:
if file:
file.close() # Always close the file
Short Takeaways:
-
try: Code that might raise exceptions -
except: Handle specific exceptions -
else: Run if no exceptions occurred -
finally: Always run (perfect for cleanup) -
raise: Create your own exceptions -
Custom Exceptions: Make your error handling more descriptive
The goal of exception handling isn't to prevent all errors, but to handle them gracefully when they occur. Good error handling makes your programs more robust and user-friendly!
Exception handling is what separates amateur code from professional, production-ready applications.
Comments (0)
No comments yet. Be the first to comment!