User-Defined Exceptions and Logging in Python
Exception Handling in Python: Custom Exceptions and Logging Best Practices
Table of contents
When dealing with application-specific fault scenarios, Python not only provides a comprehensive collection of built-in exceptions but also allows you to define your custom exceptions. Logging exceptions is also a good practice to guarantee effective error handling and debugging. In this post, we'll look at both user-defined exceptions and how to efficiently report exceptions in Python.
User-Defined Exceptions
Define a custom exception class
User-defined exceptions are custom exception classes created by inheriting from Python's basic 'Exception' class or any of its subclasses. These custom exceptions assist you in dealing with error scenarios that are peculiar to your application.
class MyCustomException(Exception):
def __init__(self, message):
super().__init__(message)
Raise the custom exception
When you encounter an error condition in your code, you can raise your custom exception using the raise
keyword. You can include a custom error message to provide information about the exception
def some_function(x):
if x < 0:
raise MyCustomException("Input should be a positive number.")
return x * 2
Handle the custom exception
A try and except block may be used to handle the custom exception. In this block, you can catch the specified exception type and take the following actions
try:
result = some_function(-5)
except MyCustomException as e:
print(f"Caught an exception: {e}")
else:
print(f"Result: {result}")
If some_function(-5) is called, it raises a MyCustomException, executing the code inside the except block and printing the error message, while some_function(10) does not raise an exception.
Example 1: Managing bank accounts
Building a library for managing bank accounts ensures balances are never negative, raising a custom exception called InsufficientFundsException if withdrawals would result in a negative balance.
class InsufficientFundsException(Exception):
def __init__(self, account_number, balance, amount):
self.account_number = account_number
self.balance = balance
self.amount = amount
super().__init__(f"Account {account_number} has insufficient funds. "
f"Balance: {balance}, Withdrawal amount: {amount}")
class BankAccount:
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self.balance = initial_balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be greater than 0.")
if self.balance < amount:
raise InsufficientFundsException(self.account_number, self.balance, amount)
self.balance -= amount
# Example usage
account1 = BankAccount("12345", 1000)
try:
account1.withdraw(1500) # This will raise InsufficientFundsException
except InsufficientFundsException as e:
print(e)
try:
account1.withdraw(500) # This will succeed
except InsufficientFundsException as e:
print(e)
else:
print(f"Withdrawal successful. New balance: {account1.balance}")
The BankAccount class raises a custom exception InsufficientFundsException when a withdrawal results in insufficient funds, providing informative error messages and allowing for handling the exception in a try and except block.
Logging Exceptions
Exception logging is an essential technique in software development. Logging allows you to capture information about errors that occur in your program, which is useful for troubleshooting and debugging.
Import the logging module
Configure the logging settings (optional)
Wrap your code in a try-except block
Add log messages
Use different log levels
Handle exceptions as needed
Import the logging module
Begin by importing the 'logging' module, which contains logging methods and classes in Python.
import logging
Configure the logging settings (optional)
You can tailor the logging settings to your requirements. You can, for example, change the log level, select a log file, or specify the log type.
logging.basicConfig(
filename='app.log', # Specify a log file
level=logging.ERROR, # Set the log level to ERROR or higher
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
This option will report only error-level and above messages to the app.log file in the format provided.
Wrap your code in a try-except block
Use a 'try' and 'except' block to surround the code that you wish to monitor for errors. Log the exception [ information in the 'except' block.
try:
# Code that may raise an exception
except Exception as e:
# Log the exception
logging.error(f"An exception occurred: {str(e)}")
If you wish to treat distinct errors differently, you may catch a specific exception type, such as except 'MyCustomException as e:'
.
Add log messages
You can report extra information about the exception, such as the stack trace or any relevant context, inside the unless block.
try:
# Code that may raise an exception
except Exception as e:
# Log the exception and additional information
logging.error(f"An exception occurred: {str(e)}", exc_info=True)
The exc_info=True parameter will provide the stack trace of the exception in the log.
Use different log levels
Depending on the severity of the error, you can utilize different log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL). For example, for significant mistakes, use logging.error() and logging.warning() for less serious concerns.
import logging
# Configure the logging settings
logging.basicConfig(
filename='app.log',
level=logging.DEBUG, # Set the lowest log level to DEBUG
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
class CustomException(Exception):
pass
def perform_operation(value):
try:
result = 10 / value # Example operation that may raise an exception
logging.debug("Operation succeeded: %s", result)
except ZeroDivisionError:
logging.warning("Division by zero occurred.")
result = None # Provide a fallback value or behavior
except Exception as e:
logging.error("An unexpected error occurred: %s", str(e))
raise
return result
def higher_level_function():
try:
result = perform_operation(0)
if result is None:
logging.warning("Fallback behavior: Unable to perform the operation.")
else:
logging.info("Result of the operation: %s", result)
except CustomException as e:
logging.error("Custom exception handled: %s", str(e))
except Exception as e:
logging.critical("An unexpected error occurred in higher_level_function: %s", str(e))
if __name__ == "__main__":
try:
higher_level_function()
except Exception as e:
logging.critical("An error occurred in the main application: %s", str(e))
Handle exceptions as needed
Depending on the needs of your application, you may opt to handle errors graciously (for example, by offering a fallback behavior) or propagate them up the call stack.
import logging
class CustomException(Exception):
pass
def perform_operation(value):
try:
result = 10 / value # Example operation that may raise an exception
except ZeroDivisionError:
logging.error("Division by zero occurred.")
result = None # Provide a fallback value or behavior
except Exception as e:
logging.error(f"An unexpected error occurred: {str(e)}")
raise # Propagate the exception up the call stack for higher-level code to handle
return result
def higher_level_function():
try:
result = perform_operation(0) # Call the function that may raise an exception
if result is None:
print("Fallback behavior: Unable to perform the operation.")
else:
print(f"Result of the operation: {result}")
except CustomException as e:
print(f"Custom exception handled: {str(e)}")
except Exception as e:
print(f"An unexpected error occurred in higher_level_function: {str(e)}")
if __name__ == "__main__":
try:
higher_level_function() # Call the higher-level function
except Exception as e:
logging.error(f"An error occurred in the main application: {str(e)}")
The assert Statement
The assert
statement in Python checks if a condition is True, raising an AssertionError
exception if False, used for debugging during development and testing, but not for production error handling.
assert condition, message
condition
is the expression to check.message
(optional) provides additional error information.
def divide(a, b):
assert b != 0, "Division by zero is not allowed"
return a / b
result = divide(10, 2) # No error
result = divide(10, 0) # AssertionError with the specified message