Table of Contents
- What Are Exceptions in Python?
- The
try-exceptBlock: Basic Error Handling - Handling Specific Exceptions
- The
elseClause: Code for Success - The
finallyClause: Cleanup Operations - Raising Exceptions with
raise - Custom Exceptions: Defining Your Own Errors
- Best Practices for Exception Handling
- Common Pitfalls to Avoid
- Conclusion
- References
What Are Exceptions in Python?
Before diving into handling exceptions, let’s clarify what they are. In Python, exceptions are objects that represent errors. They are raised (triggered) when the interpreter encounters an unexpected condition. Unlike syntax errors (which occur when code violates Python’s grammar rules and prevent the program from running), exceptions occur during execution.
Examples of Built-in Exceptions
Python has numerous built-in exceptions to handle common errors. Here are a few:
ZeroDivisionError: Raised when dividing by zero.TypeError: Raised when an operation is performed on an incompatible type (e.g., adding a string and an integer).ValueError: Raised when a function receives an argument of the correct type but an invalid value (e.g., converting “abc” to an integer withint("abc")).IndexError: Raised when accessing a sequence (list, string) with an out-of-range index.FileNotFoundError: Raised when trying to open a non-existent file.
All exceptions in Python inherit from the base class BaseException, with Exception (a subclass of BaseException) being the parent of most user-handleable errors.
The try-except Block: Basic Error Handling
The core of exception handling in Python is the try-except block. It allows you to “try” running a block of code and “catch” exceptions if they occur.
Syntax:
try:
# Code that might raise an exception
risky_operation()
except ExceptionType:
# Code to handle the exception
handle_error()
How It Works:
- The code inside
tryruns first. - If no exception occurs, the
exceptblock is skipped. - If an exception of type
ExceptionTypeoccurs, thetryblock stops, and theexceptblock executes.
Example: Handling Division by Zero
try:
numerator = 10
denominator = 0
result = numerator / denominator
print(f"Result: {result}") # This line won't run
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
# Output: Error: Cannot divide by zero!
Here, dividing by zero raises a ZeroDivisionError, which is caught by the except block, preventing the program from crashing.
Handling Specific Exceptions
Catching a broad Exception type (or using a bare except:) is risky because it can mask unexpected errors (e.g., typos, logic bugs). Instead, always catch specific exceptions to handle known errors explicitly.
Catching Multiple Specific Exceptions
You can catch multiple exceptions by listing them in a tuple inside except:
try:
user_input = input("Enter a number: ")
number = int(user_input) # May raise ValueError if input is not a number
result = 10 / number # May raise ZeroDivisionError if number is 0
print(f"10 divided by {number} is {result}")
except ValueError:
print("Error: Invalid input. Please enter a valid number.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
# Example 1: Input "abc" → Output: Error: Invalid input. Please enter a valid number.
# Example 2: Input "0" → Output: Error: Cannot divide by zero.
Catching Exceptions with a Variable
To access details about the exception (e.g., error message), use as to assign the exception instance to a variable:
try:
int("not_a_number")
except ValueError as e:
print(f"Conversion failed: {e}") # e contains the error message
# Output: Conversion failed: invalid literal for int() with base 10: 'not_a_number'
The else Clause: Code for Success
The else clause (optional) runs only if no exceptions were raised in the try block. 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 exception occurred
success_operation()
Example:
try:
number = int(input("Enter a positive number: "))
except ValueError:
print("Invalid input: Please enter a number.")
else:
if number > 0:
print(f"Thanks! You entered {number}.")
else:
print("Number is not positive.")
# If input is "5" → Output: Thanks! You entered 5.
# If input is "abc" → Output: Invalid input: Please enter a number.
Here, the else block runs only if int(input(...)) succeeds (no ValueError), ensuring we check positivity only for valid numbers.
The finally Clause: Cleanup Operations
The finally clause (optional) runs always, regardless of whether an exception occurred. It’s ideal for cleanup tasks like closing files, releasing network connections, or unlocking resources.
Syntax:
try:
risky_operation()
except ExceptionType:
handle_error()
else:
success_operation()
finally:
# Runs no matter what (success or failure)
cleanup_operation()
Example: Closing a File
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.")
# If file exists → Output: File read successfully. \n File closed.
# If file doesn't exist → Output: Error: File not found. \n File closed.
Even if the file isn’t found, finally ensures the file handle is closed (preventing resource leaks). For files, Python’s with statement is preferred (it auto-closes files), but finally is useful for other cleanup tasks (e.g., database connections).
Raising Exceptions with raise
Sometimes, you’ll want to manually trigger exceptions—for example, to enforce validation rules. Use the raise statement to throw an exception, optionally with a custom message.
Syntax:
raise ExceptionType("Custom error message")
Example: Validating User Input
def get_positive_number():
number = int(input("Enter a positive number: "))
if number <= 0:
raise ValueError(f"Number {number} is not positive.")
return number
try:
num = get_positive_number()
print(f"You entered {num}.")
except ValueError as e:
print(f"Invalid input: {e}")
# If input is "-5" → Output: Invalid input: Number -5 is not positive.
Here, get_positive_number() raises a ValueError if the input is non-positive, which is then caught and handled in the try-except block.
Custom Exceptions: Defining Your Own Errors
For domain-specific errors (e.g., payment failures, invalid user roles), define custom exceptions by inheriting from Python’s Exception class (or a subclass like ValueError). Custom exceptions make error handling more readable and precise.
Syntax:
class CustomException(Exception):
"""Base class for custom exceptions."""
pass
class SpecificError(CustomException):
"""Raised when a specific error condition occurs."""
def __init__(self, message):
self.message = message
Example: Payment Processing
class PaymentError(Exception):
"""Base exception for payment-related errors."""
pass
class InsufficientFundsError(PaymentError):
"""Raised when a user has insufficient funds."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Insufficient funds: Balance {balance}, Attempted {amount}")
def process_payment(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
print(f"Payment of {amount} processed successfully.")
try:
process_payment(100, 150)
except InsufficientFundsError as e:
print(f"Payment failed: {e}")
# Output: Payment failed: Insufficient funds: Balance 100, Attempted 150
By defining InsufficientFundsError, we make error messages more descriptive and allow callers to handle payment-specific errors separately from other issues.
Best Practices for Exception Handling
To write clean, maintainable exception-handling code, follow these practices:
1. Catch Specific Exceptions
Avoid bare except: or except Exception:—they catch all exceptions (including KeyboardInterrupt or SystemExit), masking bugs. Catch only the exceptions you expect.
Bad:
try:
risky_code()
except: # Catches ALL exceptions (even KeyboardInterrupt!)
print("Something went wrong.") # Hides the root cause
Good:
try:
risky_code()
except (ValueError, TypeError) as e: # Specific exceptions
print(f"Expected error: {e}")
2. Avoid Empty except Blocks
Never use except: without handling the error—it silently ignores issues, making debugging impossible.
Bad:
try:
risky_code()
except:
pass # Error is suppressed!
3. Use finally for Cleanup
Always release resources (files, connections) in finally to prevent leaks, even if an error occurs.
4. Document Exceptions
Use docstrings to document which exceptions a function may raise, helping users handle them.
def divide(a, b):
"""Divides a by b.
Args:
a (int/float): Dividend.
b (int/float): Divisor.
Raises:
ZeroDivisionError: If b is zero.
"""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero.")
return a / b
5. Handle Exceptions Close to the Source
Catch exceptions as close to where they occur as possible. This makes debugging easier (you know exactly where the error happened).
6. Don’t Suppress Exceptions
Only handle exceptions you can recover from. If you can’t fix the issue, let the exception propagate up to the caller.
Common Pitfalls to Avoid
1. Overusing try-except
Wrapping every line in try-except bloats code and hides logic errors. Use try-except only for operations that might fail (e.g., I/O, network calls, user input).
2. Raising Generic Exceptions
Raising Exception("Error") is unhelpful. Use specific built-in exceptions (e.g., ValueError) or custom exceptions to provide context.
3. Catching Exception (Too Broad)
except Exception: catches all non-system-exiting exceptions, including bugs like NameError or KeyError that should be fixed, not handled.
Conclusion
Exception handling is a cornerstone of robust Python programming. By using try-except-else-finally, raising meaningful exceptions, and following best practices, you can write code that gracefully handles errors, improves user experience, and simplifies debugging. Remember: the goal isn’t to avoid exceptions, but to handle them in a way that keeps your program stable and informative.