2

A guide to exception handling in Python

 9 months ago
source link: https://www.honeybadger.io/blog/a-guide-to-exception-handling-in-python/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A guide to exception handling in Python

Discover powerful exception-handling techniques in Python with this comprehensive guide. Learn about nested try-except blocks, catching and re-raising exceptions, handling exceptions in loops and async code, creating custom exceptions, and more.

Exceptions can occur for various reasons, such as invalid input, logical errors, file handling issues, network problems, or other exception conditions. Examples of exceptions in Python include ZeroDivisionError, TypeError, FileNotFoundError, and ValueError, among others. Exception handling is a crucial aspect of writing robust and reliable code in Python.

This tutorial aims to provide a comprehensive understanding of exception-handling techniques and provide examples of how those techniques can be used practically.

What is an exception?

In Python, an exception is an event that occurs during the execution of a program and disrupts the normal flow of the program. It represents an error or an exception condition that the program encounters and cannot handle by itself.

When an exception occurs, it is "raised" or "thrown" by the Python interpreter. The exception then propagates up the call stack, searching for an exception handler that can catch and handle the exception. If no suitable exception handler is found, the program terminates, and an error message is displayed.

Here's an example of a ZeroDivisionError exception being raised and handled using a try-except block:

try:
    result = 10 / 0  # Raises ZeroDivisionError
except ZeroDivisionError:
    print("Error: Division by zero!")

In this example, the code within the try block raises a ZeroDivisionError exception when attempting to divide by zero. The exception is caught by the except block, and the specified error message is printed, allowing the program to continue execution instead of abruptly terminating.

Nested Try-Except Blocks

Nested try-except blocks provide a way to handle specific exceptions at different levels of code execution. This technique allows you to catch and handle exceptions more precisely based on the context in which they occur. Consider the following example:

try:
    # Outer try block
    try:
        # Inner try block
        file = open("nonexistent_file.txt", "r")
        content = file.read()
        file.close()
        print("File content:", content)
    except FileNotFoundError:
        print("Error: File not found!")
except:
    print("Error: Outer exception occurred!")

Output:

Error: File not found!

In this example, the inner try block attempts to open a file "nonexistent_file.txt" in read mode, which doesn't exist and raises a FileNotFoundError. The exception is caught by the inner except block, which prints the error message "Error: File not found!".

Since the exception is handled within the inner except block, the outer except block is not executed. However, if the inner except block was not executed, the exception would propagate to the outer except block, and the code within the outer except block would be executed.

“Everyone is in love with Honeybadger ... the UI is spot on.” 
molly-struve.jpg?1693791196Molly Struve, Sr. Site Reliability Engineer, Netflix
Start free trial

Catching and Re-Raising Exceptions

Catching and re-raising exceptions is a useful technique when you need to handle an exception at a specific level of code execution, perform certain actions, and then allow the exception to reproduce to higher levels for further handling. Let's explore the example further and discuss its significance.

In the provided code snippet, the validate_age function takes an age parameter and checks if it is negative. If the age is negative, a ValueError is raised using the raise keyword. The exception is then caught by the except block that specifies ValueError as the exception type.

def validate_age(age):
    try:
        if age < 0:
            raise ValueError("Age cannot be negative!")
    except ValueError as ve:
        print("Error:", ve)
        raise  # Re-raise the exception

try:
    validate_age(-5)
except ValueError:
    print("Caught the re-raised exception!")

In this case, if the age provided to validate_age is -5, the condition if age < 0 is satisfied, and a ValueError is raised with the message "Age cannot be negative!".

The except block then catches the ValueError and prints the error message using print("Error:", ve). This step allows you to perform specific actions, such as logging the error or displaying a user-friendly error message.

After printing the error message, the raise statement is used to re-raise the caught exception. This re-raised exception propagates to a higher level of code execution, allowing it to be caught by an outer exception handler if present.

The output of this code snippet is:

Error: Age cannot be negative!
Caught the re-raised exception!

This example demonstrates the importance of catching and re-raising exceptions. By catching an exception, performing necessary actions, and re-raising it, you have more control over how the exception is handled at different levels of your code.

Handling Exceptions in Loops

Handling exceptions in loops is essential to ensure the smooth execution of code and prevent premature termination. By incorporating exception handling within loops, you can gracefully handle specific exceptions and continue the loop iteration. Let's explore the example further to understand its significance.

numbers = [1, 2, 3, 0, 4, 5]

for num in numbers:
    try:
        result = 10 / num
        print(f"Result: {result}")
    except ZeroDivisionError:
        print("Error: Division by zero!")
    except Exception as e:
        print("Unknown Error:", e)

In this code snippet, we have a loop that iterates over a list of numbers. For each number, a division operation is performed by dividing 10 by the current number.

If the number is zero, a ZeroDivisionError is raised. The first except ZeroDivisionError block catches this exception and prints the error message "Error: Division by zero!". Additionally, there is a generic except Exception as e block to catch any other exceptions that may occur during the loop iteration.

The output of this code snippet is:

Result: 10.0
Result: 5.0
Result: 3.3333333333333335
Error: Division by zero!
Result: 2.5
Result: 2.0

As we can see from the output, the loop successfully performs the division operation for non-zero numbers and prints the results. When encountering a zero, the ZeroDivisionError is caught, and the error message is printed. The loop continues to execute, handling each iteration gracefully, even in the presence of exceptions.

Handling exceptions in asynchronous code (asyncio)

Handling exceptions in asynchronous code can be crucial to ensure the stability and proper functioning of asynchronous tasks. The asyncio library in Python provides tools and mechanisms to handle exceptions in async tasks effectively.

import asyncio

async def divide(a, b):
    return a / b

async def main():
    try:
        await divide(10, 0)
    except ZeroDivisionError:
        print("Error: Division by zero!")

asyncio.run(main())

In this code snippet, we have an asynchronous function named divide that performs a division operation on two numbers. The division is executed using the division operator /, which may raise a ZeroDivisionError if the denominator is zero.

Within this function, we use the try-except block to catch any ZeroDivisionError that may occur during the execution of the divide coroutine.

The output of this code snippet is:

Error: Division by zero!

As expected, the division by zero raises a ZeroDivisionError exception, which is caught by the except ZeroDivisionError block. The error message "Error: Division by zero!" is then printed. Handling exceptions in asynchronous code with asyncio involves using the try-except block within the context of an async task.

Get Honeybadger's best Python articles in your inbox

We publish 1-2 times per month. Subscribe to get our Python articles as soon as we publish them.

We're Honeybadger. We'll never send you spam; we will send you cool stuff like exclusive content, memes, and swag.

Developing custom exceptions

Python allows developers to create custom exception classes to handle specific types of errors within their applications. Creating custom exception classes provides a way to organize and categorize errors, making code more maintainable and enabling more granular error handling. With this in mind, let's now explore the process of creating custom exception classes in Python.

To create a custom exception class, you need to define a new class that inherits from one of the built-in exception classes or the base Exception class. Here is an example:

import logging

class CustomException(Exception):
    def __init__(self, message):
        super().__init__(message)
        self.error_code = 1001


    def log_error(self):
        logger = logging.getLogger("custom_logger")
        logger.error(f"Custom Exception occurred: {self}")
def divide(a, b):
    if b == 0:
        raise CustomException("Division by zero is not allowed.")
    return a / b

try:
    result = divide(10, 0)
except CustomException as ce:
    print("Error code:", ce.error_code)
    ce.log_error()

Output:

Error code: 1001
Custom Exception occurred: Division by zero is not allowed.

In the code above, the CustomException class is defined, inheriting from the base Exception class. It has an additional attribute, error_code, which is set to 1001 in the constructor (__init__ method). The super().__init__(message) call initializes the base Exception class with the given error message.

The log_error method retrieves or creates a logger named "custom_logger" using logging.getLogger("custom_logger"). The logger.error method is used to log an error message indicating that a custom exception occurred. The {self} expression in the log message includes the string representation of the exception object.

The divide function performs division between two numbers. If the divisor (b) is zero, the CustomException is triggered with the error message "Division by zero is not allowed."

The division operation is attempted within a try-except block. If a CustomException is raised, it is caught using the except CustomException as ce block. The error code of the exception is printed (print("Error code:", ce.error_code)) to demonstrate access to the error_code attribute of the CustomException instance. Finally, the log_error method is invoked (ce.log_error()) to log the exception.

Logging exceptions to an error monitoring service

It’s inevitable that you’ll come across errors and exceptions when developing software applications. Monitoring and managing these errors effectively is crucial for maintaining the stability and performance of an application. One effective approach is to log exceptions to an error-monitoring service. Such services provide valuable insights into the occurrence and impact of errors, enabling developers to identify and resolve issues promptly. By integrating an error-monitoring service into the development workflow, teams can enhance their debugging and troubleshooting capabilities, leading to improved user experience and overall application quality. Examples of some error monitoring services are Honeybadger, Sentry, Datadog, etc.

Integrating Honeybadger for error monitoring

Honeybadger is a powerful error-monitoring tool for Python applications. Integrating an error monitoring service like Honeybadger into your development workflow provides numerous benefits for effectively managing exceptions. From real-time notifications and error grouping to rich diagnostics and trend analysis, Honeybadger equips you with the tools you need to quickly identify, investigate, and resolve errors and ultimately enhance the overall quality and reliability of your applications. To demo this, let's now explore some features and examples of integrating Honeybadger into your Python code.

To use Honeybadger you need to first get an API key, which you get by signing up to Honeybadger. On the Honeybadger projects page, click on the “Create your first project” button, give your project a name, and finally click the “Settings” tab, and you will be able to access your API key there.

You will also need to install Honeybadger locally. You can do that with the following command:

pip install honeybadger
Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
Start free trial

Customizing Error Notifications

Honeybadger allows you to customize error notifications based on your application's needs. You can specify the severity level, tags, and additional context to be included in the error reports. Here's an example:

from honeybadger import honeybadger

# Configure Honeybadger with your API key
honeybadger.configure(api_key='hbp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX') # paste your API key

def divide(a, b):
    result = a / b
    return result        

# Example usage of the divide function
try:
    result = divide(10, 0)
except Exception as e:
    honeybadger.notify(e, context={'user_id': 123}, error_message='Division by zero is not allowed.') #'Division by zero is not allowed.' will show if `e` was not passed

In this example, honeybadger.notify() the context parameter provides additional contextual information, such as the user ID associated with the error. To manually send an error message to Honeybadger notification, you add the “error_message argument. The message in error_message will show up only when there is no error passed in the honeybadger.notify() method.

When you run the above code, you will receive in your email a message notifying you of the error, like the message that can be seen below.

Exception error from honeybadger

Conclusion

Exception handling is a critical aspect of writing robust Python code. By mastering various techniques, from nested try-except blocks to creating custom exceptions, you can enhance the reliability of your code. Additionally, tools such as Honeybadger can aid in error monitoring, allowing you to proactively identify and address issues in your Python applications.

You can check out more applications of Honeybadger in the documentation.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK