Table of Contents
- What Are Exceptions?
- Basic Exception Handling:
try-exceptBlocks - Handling Multiple Exceptions
- The
elseClause - The
finallyClause - Raising Exceptions Explicitly
- Custom Exceptions
- Python’s Exception Hierarchy
- Best Practices for Exception Handling
- Common Pitfalls to Avoid
- Conclusion
- References
What Are Exceptions?
An exception is an event that disrupts the normal flow of a program’s execution. In Python, exceptions are objects that represent errors. When an error occurs, Python raises (or “throws”) an exception. If the exception is not handled, the program terminates and displays a traceback—a detailed report of the error type, message, and the line of code where it occurred.
Exceptions vs. Syntax Errors
-
Syntax Errors: Occur when code violates Python’s grammar rules (e.g., missing colons, incorrect indentation). These prevent the program from running at all.
Example:if x == 5 # Missing colon → SyntaxError print("Hello") -
Exceptions: Occur during execution, even if the code is syntactically correct. For example:
10 / 0 # Division by zero → ZeroDivisionError
Basic Exception Handling with try-except
Python’s primary tool for handling exceptions is the try-except block. It allows you to “try” running potentially error-prone code and “catch” exceptions if they occur, preventing the program from crashing.
Syntax:
try:
# Code that might raise an exception
risky_operation()
except ExceptionType:
# Code to run if ExceptionType occurs
handle_error()
How It Works:
- The
tryblock executes first. If no exceptions occur, theexceptblock is skipped. - If an exception of type
ExceptionTypeoccurs in thetryblock, the rest of thetryblock is aborted, and theexceptblock runs.
Example: Handling Division by Zero
def safe_divide(a, b):
try:
result = a / b
print(f"Result: {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
safe_divide(10, 2) # Output: Result: 5.0 (no exception)
safe_divide(10, 0) # Output: Error: Cannot divide by zero! (ZeroDivisionError caught)
Handling Multiple Exceptions
A single try block can raise multiple types of exceptions. You can handle them separately with multiple except clauses.
Syntax:
try:
# Risky code
except ExceptionType1:
# Handle ExceptionType1
except ExceptionType2:
# Handle ExceptionType2
Example: Handling Different Input Errors
Suppose you’re converting user input to an integer. Possible exceptions:
ValueError: If input is non-numeric (e.g.,"abc").TypeError: If input is not a string/number (e.g.,None).
def convert_to_int(input_data):
try:
return int(input_data)
except ValueError:
print(f"Error: '{input_data}' is not a valid integer.")
except TypeError:
print(f"Error: Expected string/number, got {type(input_data).__name__}.")
convert_to_int("123") # Output: 123 (no exception)
convert_to_int("abc") # Output: Error: 'abc' is not a valid integer. (ValueError)
convert_to_int(None) # Output: Error: Expected string/number, got NoneType. (TypeError)
The else Clause
The else clause (optional) runs only if the try block completes without raising an exception. It separates code that might fail from code that should run only on success.
Syntax:
try:
risky_operation()
except ExceptionType:
handle_error()
else:
# Runs only if no exceptions occurred in try
success_operation()
Example: Processing Data After Safe Input
def process_positive_number():
try:
num = float(input("Enter a positive number: "))
except ValueError:
print("Error: Invalid input. Please enter a number.")
else:
if num > 0:
print(f"Square root: {num ** 0.5}")
else:
print("Error: Number must be positive.")
process_positive_number()
# If input is "25": Square root: 5.0
# If input is "abc": Error: Invalid input. Please enter a number.
# If input is "-4": Error: Number must be positive.
The finally Clause
The finally clause (optional) runs regardless of whether an exception occurred in the try block. It is ideal for cleanup tasks like closing files, releasing network connections, or freeing resources.
Syntax:
try:
risky_operation()
except ExceptionType:
handle_error()
else:
success_operation()
finally:
# Runs always (cleanup code)
cleanup()
Example: Closing a File Safely
Files must be closed after use to avoid resource leaks. finally ensures the file is closed even if an error occurs during reading:
file = None
try:
file = open("data.txt", "r")
content = file.read()
print("File read successfully.")
except FileNotFoundError:
print("Error: File not found.")
finally:
if file is not None:
file.close()
print("File closed.")
# Output if file exists:
# File read successfully.
# File closed.
# Output if file missing:
# Error: File not found.
# File closed.
Note: In modern Python, the with statement (context manager) is preferred for file handling, as it auto-closes files. However, finally is still useful for non-context-manager resources.
Raising Exceptions Explicitly
You can intentionally raise exceptions with the raise statement. This is useful when validating inputs or enforcing business rules (e.g., “a user’s age cannot be negative”).
Syntax:
raise ExceptionType("Optional error message")
Example: Validating Age
def set_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer.")
if age < 0:
raise ValueError("Age cannot be negative.")
print(f"Age set to {age}.")
try:
set_age(-5) # Raises ValueError
except ValueError as e:
print(f"Error: {e}") # Output: Error: Age cannot be negative.
Re-raising Exceptions
Sometimes you may want to catch an exception, log it, and then re-raise it to let the caller handle it. Use raise without arguments to preserve the original traceback:
try:
10 / 0
except ZeroDivisionError as e:
print(f"Logging error: {e}")
raise # Re-raise the exception
# Output:
# Logging error: division by zero
# Traceback (most recent call last): ... ZeroDivisionError: division by zero
Custom Exceptions
Python allows you to define custom exceptions by subclassing the built-in Exception class (or its subclasses). Custom exceptions make error handling more readable and enable granular error management for application-specific scenarios.
Why Custom Exceptions?
- Clarity: They signal specific errors (e.g.,
InsufficientFundsErrorvs. a genericValueError). - Control: Callers can handle them explicitly with
exceptclauses.
Example: Banking App Exceptions
class BankingError(Exception):
"""Base class for banking-related exceptions."""
pass
class InsufficientFundsError(BankingError):
"""Raised when an account has insufficient funds for a transaction."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Insufficient funds. Balance: {balance}, Attempted withdrawal: {amount}")
class Account:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
print(f"Withdrew {amount}. New balance: {self.balance}")
# Usage
account = Account(100)
try:
account.withdraw(150) # Raises InsufficientFundsError
except InsufficientFundsError as e:
print(f"Banking Error: {e}")
# Output: Banking Error: Insufficient funds. Balance: 100, Attempted withdrawal: 150
Python’s Exception Hierarchy
Python exceptions form a hierarchy, with BaseException at the root. Most user-defined exceptions subclass Exception (a subclass of BaseException), which excludes system-exiting exceptions like KeyboardInterrupt (Ctrl+C) and SystemExit.
Key Levels of the Hierarchy:
BaseException
├── Exception (most user exceptions subclass this)
│ ├── ArithmeticError
│ │ ├── ZeroDivisionError
│ │ └── OverflowError
│ ├── ValueError
│ ├── TypeError
│ ├── FileNotFoundError (subclass of OSError)
│ └── ... (other built-in exceptions)
├── KeyboardInterrupt
└── SystemExit
Catching Broad vs. Specific Exceptions
- Catching
Exception(or no type) will catch all non-system-exiting exceptions, but this is discouraged (see Best Practices). - Always catch the most specific exception possible to avoid masking bugs.
Best Practices for Exception Handling
-
Avoid Bare
exceptClauses
A bareexcept:catches all exceptions, includingKeyboardInterrupt(which users use to exit programs) andSystemExit. This can make debugging impossible:try: risky_code() except: # Bad! Catches everything print("Something went wrong.")Instead, catch specific exceptions or
Exception(if necessary), but never bareexcept. -
Use
finallyfor Cleanup
Always usefinally(or context managers) to release resources like files, locks, or network connections. -
Provide Descriptive Error Messages
Include context in error messages to aid debugging (e.g., “Failed to read file ‘data.txt’” instead of “File error”). -
Document Exceptions in Docstrings
Tell users which exceptions your functions may raise:def withdraw(amount): """Withdraw funds from the account. Args: amount (float): Amount to withdraw. Raises: InsufficientFundsError: If amount exceeds balance. ValueError: If amount is negative. """ ... -
Use Custom Exceptions for App-Specific Logic
Custom exceptions make error handling more expressive and allow callers to handle specific cases.
Common Pitfalls to Avoid
- Swallowing Exceptions: Catching an exception and doing nothing (e.g.,
except Exception: pass) hides bugs and makes debugging hard. - Overusing Exceptions for Control Flow: Don’t use exceptions to handle expected cases (e.g., “if a file exists, read it” should use
os.path.exists()instead of atry-exceptaroundopen()). - Catching
BaseException: This includesKeyboardInterruptandSystemExit, preventing users from exiting or scripts from terminating.
Conclusion
Exception handling is a cornerstone of writing robust, user-friendly Python code. By mastering try-except, else, finally, and custom exceptions, you can gracefully handle errors, clean up resources, and communicate issues clearly. Remember to prioritize specificity, document exceptions, and avoid common pitfalls like bare except clauses. With these tools, you’ll build programs that are resilient to the unexpected.