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.
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)
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
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
account1 = BankAccount("12345", 1000)
try:
account1.withdraw(1500)
except InsufficientFundsException as e:
print(e)
try:
account1.withdraw(500)
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.
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
Begin by importing the 'logging' module, which contains logging methods and classes in Python.
import logging
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',
level=logging.ERROR,
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.
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:
except Exception as e:
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:'.
You can report extra information about the exception, such as the stack trace or any relevant context, inside the unless block.
try:
except Exception as e:
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
logging.basicConfig(
filename='app.log',
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
class CustomException(Exception):
pass
def perform_operation(value):
try:
result = 10 / value
logging.debug("Operation succeeded: %s", result)
except ZeroDivisionError:
logging.warning("Division by zero occurred.")
result = None
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
except ZeroDivisionError:
logging.error("Division by zero occurred.")
result = None
except Exception as e:
logging.error(f"An unexpected error occurred: {str(e)}")
raise
return result
def higher_level_function():
try:
result = perform_operation(0)
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()
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
def divide(a, b):
assert b != 0, "Division by zero is not allowed"
return a / b
result = divide(10, 2)
result = divide(10, 0)
Summary
User-defined
exceptions allow you to gracefully handle application-specific mistakes while recording
exceptions with
Python's logging module allows you to collect fault data for debugging and maintenance. Combining these strategies strengthens and maintains the resilience and maintainability of your
Python programs.