Table of Contents
- What are Exceptions?
- The Basics:
try-exceptBlock - The
elseClause: Code When No Exceptions Occur - The
finallyClause: Cleanup Actions - Raising Exceptions with
raise - Custom Exceptions
- Common Built-in Exceptions in the Standard Library
- Exception Hierarchies: Understanding Inheritance
- Context Managers and Exception Handling (
try-finallyvs.with) - Best Practices for Effective Exception Handling
- Conclusion
- References
1. What are Exceptions?
Exceptions are runtime errors that disrupt the normal execution flow of a program. They occur when the interpreter encounters an operation it cannot complete. Unlike syntax errors (which are caught before execution, e.g., missing colons or incorrect indentation), exceptions occur during execution.
Example of an Exception:
# This code will raise a ZeroDivisionError
result = 10 / 0 # Division by zero is undefined
When this runs, Python raises a ZeroDivisionError and terminates the program with an error message:
ZeroDivisionError: division by zero
Other common exceptions include IndexError (accessing an out-of-bounds list index), KeyError (accessing a non-existent dictionary key), and FileNotFoundError (opening a missing file).
2. The Basics: try-except Block
The try-except block is Python’s primary mechanism for handling exceptions. It allows you to “try” running risky code and “catch” exceptions if they occur, preventing program crashes.
Syntax:
try:
# Code that might raise an exception
risky_operation()
except ExceptionType:
# Code to handle the exception
handle_error()
Example: Handling ZeroDivisionError
try:
numerator = 10
denominator = 0
result = numerator / denominator
print(f"Result: {result}") # This line won't run if an exception occurs
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
Output:
Error: Cannot divide by zero!
2.1 Catching Specific Exceptions
Always catch specific exceptions rather than broad ones. This ensures you only handle the errors you expect, avoiding accidental suppression of unrelated issues.
Example: Catching ValueError
try:
age = int(input("Enter your age: ")) # May raise ValueError if input is not an integer
print(f"Your age is {age}")
except ValueError:
print("Error: Please enter a valid integer for age.")
If the user enters “twenty”, the int() conversion fails, and the ValueError is caught:
Enter your age: twenty
Error: Please enter a valid integer for age.
2.2 Catching Multiple Exceptions
To handle multiple exception types, list them in a tuple within a single except block or use separate except blocks.
Option 1: Tuple of Exceptions
try:
data = [1, 2, 3]
index = int(input("Enter an index: "))
print(f"Value at index {index}: {data[index]}")
except (ValueError, IndexError) as e: # Catch both ValueError and IndexError
print(f"Error: {e}")
Option 2: Separate except Blocks
try:
data = [1, 2, 3]
index = int(input("Enter an index: "))
print(f"Value at index {index}: {data[index]}")
except ValueError:
print("Error: Index must be an integer.")
except IndexError:
print("Error: Index out of bounds.")
2.3 The as Keyword: Accessing Exception Details
Use as e to capture the exception instance, which contains details about the error (e.g., error message).
Example:
try:
10 / 0
except ZeroDivisionError as e:
print(f"Exception caught: {e}") # Print the error message
print(f"Exception type: {type(e)}") # Print the exception type
Output:
Exception caught: division by zero
Exception type: <class 'ZeroDivisionError'>
3. The else Clause: Code When No Exceptions Occur
The else clause runs only if no exceptions were raised in the try block. It separates “normal” code from error-handling code, improving readability.
Syntax:
try:
risky_operation()
except ExceptionType:
handle_error()
else:
# Runs only if no exceptions occurred
normal_operation()
Example:
try:
age = int(input("Enter your age: "))
except ValueError:
print("Invalid input.")
else:
print(f"Age validated. You are {age} years old.") # Runs only if age is an integer
Output (valid input):
Enter your age: 25
Age validated. You are 25 years old.
4. The finally Clause: Cleanup Actions
The finally clause runs regardless of whether an exception occurred. It is used for cleanup tasks like closing files, releasing network connections, or freeing resources.
Syntax:
try:
risky_operation()
except ExceptionType:
handle_error()
finally:
# Cleanup code (always runs)
cleanup()
Example: Closing a File with finally
file = None
try:
file = open("data.txt", "r") # May raise FileNotFoundError
content = file.read()
print("File content read successfully.")
except FileNotFoundError:
print("Error: File not found.")
finally:
if file is not None:
file.close() # Ensure the file is closed, even if an error occurred
print("File closed.")
Output (file not found):
Error: File not found.
File closed.
5. Raising Exceptions with raise
You can manually raise exceptions using the raise statement. This is useful for enforcing business rules or validating inputs in custom functions.
Syntax:
raise ExceptionType("Custom error message")
Example: Raising ValueError for Invalid Input
def calculate_square_root(n):
if n < 0:
# Raise a ValueError with a custom message
raise ValueError("Cannot compute square root of a negative number.")
return n **0.5
try:
result = calculate_square_root(-9)
except ValueError as e:
print(f"Error: {e}") # Output: Error: Cannot compute square root of a negative number.
5.1 Re-raising Exceptions
Sometimes, you may want to catch an exception, log it, and re-raise it to let higher-level code handle it. Use raise without arguments in the except block to re-raise the original exception.
Example:
import logging
def process_data(data):
try:
# Attempt to process data
if not data:
raise ValueError("Data cannot be empty.")
except ValueError as e:
logging.error(f"Processing failed: {e}") # Log the error
raise # Re-raise the exception for the caller to handle
try:
process_data([])
except ValueError as e:
print(f"Caught re-raised exception: {e}")
Output:
ERROR:root:Processing failed: Data cannot be empty.
Caught re-raised exception: Data cannot be empty.
6. Custom Exceptions
For application-specific errors, define custom exceptions by inheriting from Python’s built-in Exception class (or a subclass like ValueError). Custom exceptions make error handling more expressive and help distinguish between different failure modes.
6.1 Defining Custom Exception Classes
Custom exceptions are typically empty classes (or with minimal logic) that inherit from Exception.
Example:
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds for a transaction."""
pass
For more context, add an __init__ method to include details like balance and transaction amount:
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Insufficient funds: Balance={balance}, Attempted withdrawal={amount}")
6.2 Using Custom Exceptions in Practice
Example: Banking Application
class BankingError(Exception):
"""Base class for all banking-related exceptions."""
pass
class InsufficientFundsError(BankingError):
"""Raised when withdrawal amount exceeds account balance."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Cannot withdraw {amount}. Balance: {balance}")
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
new_balance = withdraw(100, 150) # Withdrawal exceeds balance
except InsufficientFundsError as e:
print(f"Withdrawal failed: {e}") # Output: Withdrawal failed: Cannot withdraw 150. Balance: 100
7. Common Built-in Exceptions in the Standard Library
Python’s standard library includes a wide range of built-in exceptions. Below are key categories with examples:
7.1 Base Exceptions
BaseException: The root of all exceptions (do not catch this directly).Exception: Base class for all non-system-exiting exceptions (use this for most custom exceptions).KeyboardInterrupt: Raised when the user pressesCtrl+Cto interrupt the program.SystemExit: Raised bysys.exit()to terminate the program.
7.2 Arithmetic Errors
ArithmeticError: Base class for arithmetic-related errors.ZeroDivisionError: Division by zero (e.g.,1 / 0).OverflowError: Result of an arithmetic operation is too large (rare in Python due to arbitrary-precision integers).
7.3 Type and Value Errors
TypeError: Operation performed on an incompatible type (e.g.,"5" + 3).ValueError: Valid type but invalid value (e.g.,int("abc")ormath.sqrt(-1)).
7.4 I/O Errors
OSError: Base class for operating system errors (e.g., file issues).FileNotFoundError: Opening a non-existent file (e.g.,open("missing.txt")).PermissionError: No permission to access a file (e.g., writing to a read-only file).
7.5 Lookup Errors
LookupError: Base class for errors when accessing collections.IndexError: Accessing an out-of-bounds sequence index (e.g.,[1, 2, 3][5]).KeyError: Accessing a non-existent dictionary key (e.g.,{"a": 1}["b"]).
8. Exception Hierarchies: Understanding Inheritance
Exceptions in Python form a hierarchy via inheritance. For example, ZeroDivisionError inherits from ArithmeticError, which inherits from Exception, which inherits from BaseException.
Simplified Exception Hierarchy:
BaseException
├── Exception
│ ├── ArithmeticError
│ │ ├── ZeroDivisionError
│ │ └── OverflowError
│ ├── TypeError
│ ├── ValueError
│ ├── OSError
│ │ ├── FileNotFoundError
│ │ └── PermissionError
│ └── LookupError
│ ├── IndexError
│ └── KeyError
├── KeyboardInterrupt
└── SystemExit
Key Takeaway:
Catching a parent exception (e.g., ArithmeticError) will also catch its subclasses (e.g., ZeroDivisionError). Always order except blocks from most specific to least specific to avoid masking errors.
Example:
try:
result = 10 / 0
except ArithmeticError: # Catches ZeroDivisionError (a subclass)
print("An arithmetic error occurred.")
except ZeroDivisionError: # This block will NEVER run (ArithmeticError is broader)
print("Division by zero.")
Output:
An arithmetic error occurred.
9. Context Managers and Exception Handling (try-finally vs. with)
Context managers (via the with statement) simplify resource cleanup (e.g., closing files, network connections) and are often preferred over try-finally for readability.
try-finally (Manual Cleanup):
file = None
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found.")
finally:
if file:
file.close() # Must manually close the file
with Statement (Automatic Cleanup):
The with statement uses context managers to automatically handle cleanup (via __enter__ and __exit__ methods). Files, sockets, and database connections often implement context managers.
try:
with open("data.txt", "r") as file: # File is auto-closed when the block exits
content = file.read()
except FileNotFoundError:
print("File not found.")
The with statement ensures file.close() is called even if an exception occurs, eliminating boilerplate.
10. Best Practices for Effective Exception Handling
1.** Catch Specific Exceptions **: Avoid bare except: (which catches all exceptions, including KeyboardInterrupt). Use except ValueError: instead of except:.
2.** Avoid Silent Failures **: Never use empty except blocks (e.g., except: pass). Log errors or handle them explicitly.
3.** Use Custom Exceptions **: For application-specific errors, define custom exceptions to make error handling more expressive.
4.** Keep try Blocks Small **: Only include code that might raise exceptions in try blocks. This avoids masking unrelated errors.
5.** Document Exceptions **: Use docstrings to document which exceptions a function may raise (e.g., """Raises ValueError if n < 0.""").
6.** Don’t Use Exceptions for Control Flow **: Use if-else for expected conditions (e.g., check if a key exists in a dict with in instead of try-except KeyError).
7.** Clean Up Resources with finally or with**: Always release resources like files or network connections to prevent leaks.
11. Conclusion
Exception handling is a cornerstone of robust Python programming. By using try-except, else, finally, and custom exceptions, you can gracefully manage errors, improve user experience, and simplify debugging. Remember to follow best practices like catching specific exceptions and cleaning up resources to write maintainable, resilient code.