📓 Python Programming - Error Handling and Debugging
1. Try-Except Blocks
Error handling prevents programs from crashing due to unexpected issues.
The basic structure:
try:
# Code that might raise an error
except:
# Code to handle the error
➤ **Example:**
```python
try:
num = int(input("Enter a number: "))
print(f"Square: {num**2}")
except Exception as e:
print("Invalid input! Please enter a number.\nException:", e)
2. Catching Specific Exceptions
It's better to catch specific exceptions to handle different errors appropriately.
try:
num = int(input("Enter a number: "))
result = 10 / num
except ValueError:
print("Please enter a valid number.")
except ZeroDivisionError:
print("Division by zero is not allowed.")
except Exception as e:
print("An unexpected error occurred:", e)
3. Using else & finally
-
else: Runs if no exceptions occur.
-
finally: Runs no matter what (cleanup code).
try:
num = int(input("Enter a positive number: "))
except ValueError:
print("Invalid input.")
else:
print(f"You entered: {num}")
finally:
print("This always runs.")
4. Generating and Creating Custom Exceptions
-
You can use
raiseto generate exceptions. -
Custom exceptions are created by defining a new class that inherits from
Exception.
➤ Raising Exceptions Example:
age = -5
if age < 0:
raise ValueError("Age cannot be negative.")
➤ Custom Exception Example:
class NegativeAgeError(Exception):
pass
age = -5
try:
if age < 0:
raise NegativeAgeError("Age cannot be negative.")
except NegativeAgeError as e:
print(e)
5. Problem-Solving Strategies
✅ Understand the error message.
✅ Use print statements or debugging tools to trace the problem.
✅ Break the code into smaller parts.
✅ Handle exceptions gracefully using try-except blocks.
✅ Anticipate common errors (e.g., invalid input, division by zero).
✅ Write custom exceptions for specific situations.
6. Common Python Error Types
Understanding common errors helps you debug faster:
-
SyntaxError: Invalid Python syntax
-
NameError: Variable not defined
-
TypeError: Wrong data type used
-
IndexError: List index out of range
-
KeyError: Dictionary key doesn't exist
-
FileNotFoundError: File doesn't exist
-
ImportError: Module can't be imported
# Examples of common errors (commented out to avoid actual errors)
# 1. SyntaxError
# print("Hello World" # Missing closing parenthesis
# 2. NameError
# print(undefined_variable) # Variable not defined
# 3. TypeError
# result = "5" + 10 # Can't add string and integer
# 4. IndexError
# my_list = [1, 2, 3]
# print(my_list[5]) # Index 5 doesn't exist
# 5. KeyError
# my_dict = {"name": "John"}
# print(my_dict["age"]) # Key 'age' doesn't exist
# 6. FileNotFoundError
# with open("nonexistent.txt", "r") as file:
# content = file.read()
# Let's demonstrate with proper error handling:
def demonstrate_errors():
# Handling IndexError
try:
my_list = [1, 2, 3]
print(my_list[5])
except IndexError as e:
print(f"IndexError caught: {e}")
# Handling KeyError
try:
my_dict = {"name": "John"}
print(my_dict["age"])
except KeyError as e:
print(f"KeyError caught: Missing key {e}")
# Handling TypeError
try:
result = "5" + 10
except TypeError as e:
print(f"TypeError caught: {e}")
demonstrate_errors()
7. Debugging Techniques
Different ways to find and fix bugs in your code:
-
Print Debugging: Add print statements to track values
-
Python Debugger (pdb): Step through code line by line
-
IDE Debugger: Use VS Code's built-in debugger
-
Logging: Better than print statements for production code
➤ Print Debugging Example:
def calculate_average(numbers):
print(f"DEBUG: Input numbers: {numbers}") # Debug print
total = 0
for i, num in enumerate(numbers):
total += num
print(f"DEBUG: Step {i+1}, added {num}, total now: {total}") # Debug print
average = total / len(numbers)
print(f"DEBUG: Final average: {average}") # Debug print
return average
# Test with debug prints
numbers = [10, 20, 30, 40]
result = calculate_average(numbers)
print(f"Average: {result}")
# Better approach: Use a debug flag
DEBUG = True
def debug_print(message):
if DEBUG:
print(f"[DEBUG] {message}")
def calculate_average_v2(numbers):
debug_print(f"Input numbers: {numbers}")
total = sum(numbers)
debug_print(f"Sum: {total}")
average = total / len(numbers)
debug_print(f"Average: {average}")
return average
print("\n--- Version 2 with debug flag ---")
result2 = calculate_average_v2([5, 15, 25])
print(f"Average: {result2}")
➤ Using Python's Logging Module:
import logging
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def divide_numbers(a, b):
logging.debug(f"Starting division: {a} / {b}")
try:
if b == 0:
logging.error("Division by zero attempted!")
raise ZeroDivisionError("Cannot divide by zero")
result = a / b
logging.info(f"Division successful: {a} / {b} = {result}")
return result
except Exception as e:
logging.exception("An error occurred during division")
raise
# Test logging
print("=== Testing with logging ===")
try:
result1 = divide_numbers(10, 2)
print(f"Result 1: {result1}")
result2 = divide_numbers(10, 0)
print(f"Result 2: {result2}")
except ZeroDivisionError as e:
print(f"Caught error: {e}")
# Different log levels
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
logging.error("This is an error message")
logging.critical("This is a critical message")
8. Assertions and Testing
Assertions help catch bugs early by checking assumptions:
-
assertstatement checks if a condition is True -
Raises
AssertionErrorif condition is False -
Used for debugging and testing
# Basic assertion examples
def calculate_factorial(n):
assert n >= 0, "Factorial is not defined for negative numbers"
assert isinstance(n, int), "Input must be an integer"
if n == 0 or n == 1:
return 1
result = 1
for i in range(2, n + 1):
result *= i
# Assert the result makes sense
assert result > 0, "Factorial should always be positive"
return result
# Test assertions
try:
print(f"5! = {calculate_factorial(5)}")
print(f"0! = {calculate_factorial(0)}")
# This will raise AssertionError
print(f"(-1)! = {calculate_factorial(-1)}")
except AssertionError as e:
print(f"Assertion failed: {e}")
# Assertions in testing
def test_factorial():
# Test cases
assert calculate_factorial(0) == 1, "0! should be 1"
assert calculate_factorial(1) == 1, "1! should be 1"
assert calculate_factorial(5) == 120, "5! should be 120"
print("All factorial tests passed!")
test_factorial()
# Assertions for debugging
def divide_positive_numbers(a, b):
assert a > 0 and b > 0, "Both numbers must be positive"
result = a / b
# Check our assumption about the result
assert result > 0, "Result should be positive when dividing positive numbers"
return result
# Test
try:
result = divide_positive_numbers(10, 2)
print(f"Division result: {result}")
# This will fail the assertion
result = divide_positive_numbers(-10, 2)
except AssertionError as e:
print(f"Assertion error: {e}")
9. Exception Chaining and Context
Preserve error context when re-raising exceptions:
-
raise ... from ...- Explicit chaining -
raise- Implicit chaining -
Helps track the original cause of errors
def read_config_file(filename):
"""Example of exception chaining"""
try:
with open(filename, 'r') as file:
import json
config = json.load(file)
return config
except FileNotFoundError as e:
# Chain the original exception
raise ValueError(f"Configuration file '{filename}' not found") from e
except json.JSONDecodeError as e:
# Chain with more context
raise ValueError(f"Invalid JSON in configuration file '{filename}'") from e
def process_data():
"""Example showing exception context"""
try:
config = read_config_file("nonexistent_config.json")
return config
except ValueError as e:
print(f"Configuration error: {e}")
print(f"Original cause: {e.__cause__}")
# Re-raise with additional context
raise RuntimeError("Failed to initialize application") from e
# Test exception chaining
try:
process_data()
except RuntimeError as e:
print(f"\nFinal error: {e}")
print(f"Caused by: {e.__cause__}")
print(f"Original cause: {e.__cause__.__cause__}")
# Example with suppress chaining
def clean_reraise():
try:
result = 10 / 0
except ZeroDivisionError:
# Suppress the original exception chain
raise ValueError("Invalid calculation") from None
try:
clean_reraise()
except ValueError as e:
print(f"\nSuppressed chain error: {e}")
print(f"Has cause: {e.__cause__}") # Will be None
10. Error Handling Best Practices
Guidelines for writing robust, maintainable code:
✅ Be specific: Catch specific exceptions, not generic Exception
✅ Fail fast: Use assertions to catch bugs early
✅ Log errors: Use logging instead of print for production code
✅ Don't ignore: Always handle or log exceptions
✅ Clean up: Use finally or context managers for cleanup
✅ Document: Specify what exceptions your functions can raise
✅ Test: Write tests for both success and failure cases
# Example of good error handling practices
import logging
class DatabaseError(Exception):
"""Custom exception for database operations"""
pass
class UserNotFoundError(DatabaseError):
"""Raised when user is not found in database"""
pass
def get_user_by_id(user_id):
"""
Retrieve user by ID from database.
Args:
user_id (int): The user ID to search for
Returns:
dict: User information
Raises:
TypeError: If user_id is not an integer
UserNotFoundError: If user is not found
DatabaseError: If database connection fails
"""
# Input validation
if not isinstance(user_id, int):
raise TypeError(f"user_id must be an integer, got {type(user_id)}")
if user_id <= 0:
raise ValueError("user_id must be positive")
# Simulate database lookup
fake_database = {1: {"name": "Alice", "email": "[email protected]"}}
try:
# Simulate potential database connection error
if user_id > 100:
raise ConnectionError("Database connection lost")
if user_id not in fake_database:
raise UserNotFoundError(f"User with ID {user_id} not found")
user = fake_database[user_id]
logging.info(f"Successfully retrieved user {user_id}")
return user
except ConnectionError as e:
logging.error(f"Database connection failed: {e}")
raise DatabaseError("Failed to connect to database") from e
# Example usage with proper error handling
def main():
logging.basicConfig(level=logging.INFO)
test_cases = [1, "invalid", -1, 2, 101]
for user_id in test_cases:
try:
print(f"\nTrying to get user {user_id}:")
user = get_user_by_id(user_id)
print(f"Success: {user}")
except TypeError as e:
print(f"Type error: {e}")
except ValueError as e:
print(f"Value error: {e}")
except UserNotFoundError as e:
print(f"User not found: {e}")
except DatabaseError as e:
print(f"Database error: {e}")
print(f"Original cause: {e.__cause__}")
# Run the example
main()
🎯 Quick Recap
✅ Use try-except to catch errors and prevent crashes
✅ Catch specific exceptions for better error handling
✅ else and finally provide structured error handling
✅ Custom exceptions allow for clear, descriptive error messages
✅ Know common Python error types for faster debugging
✅ Use debugging techniques: print statements, logging, assertions
✅ Exception chaining preserves error context
✅ Follow best practices for robust, maintainable code
✅ Debugging is an essential skill for reliable code
📝 Practice Exercises
Basic Level:
- Write a program that asks for two numbers and divides them. Handle:
- ValueError for invalid inputs
-
ZeroDivisionError for division by zero
-
Create a custom exception called
PasswordTooShortErrorif the entered password is less than 8 characters. -
Demonstrate the use of
elseandfinallyin a simple program that reads a file. -
Intentionally raise an exception if a negative number is entered for age.
Intermediate Level:
-
Write a function that catches different types of errors (IndexError, KeyError, TypeError) and prints specific messages for each.
-
Create a simple calculator with proper error handling and logging.
-
Use assertions to validate input in a function that calculates square roots.
-
Write a program that demonstrates exception chaining when processing user data.
Advanced Level:
-
Build a file processor that handles multiple types of errors and uses logging instead of print statements.
-
Create a custom exception hierarchy for a banking system (InsufficientFundsError, InvalidAccountError, etc.).
-
Write a robust function with proper documentation that specifies all possible exceptions it can raise.
-
Implement a retry mechanism that attempts an operation multiple times before giving up.