Mastering Python Error Handling: A Comprehensive Guide

With examples from Simple to Advanced

·

13 min read

Mastering Python Error Handling:  A Comprehensive Guide
A Note for the eager-minds reading this:

Before diving in, it's important to note that this guide assumes you have:

  • A fundamental understanding of programming concepts, including the declaration and assignment of variables.

  • Prior experience with an IDE(Integrated Development Environment) for Python with the ability to add and modify code blocks.

  • Proficiency in using Git for version control(to save different versions of your code).

Agenda

  • Understanding Python Exceptions

    • Introduction to exceptions

    • Common types of exceptions

    • The Try-Except Block

  • Syntax and structure

    • Handling specific exceptions

    • Multiple except blocks

    • Using the else clause

    • Raising Exceptions

  • The 'raise' statement

    • Creating custom exceptions

    • Best practices for raising exceptions

  • Exception Handling in Real-World Scenarios

    • File I/O and exception handling

    • Network operations and error management

    • Database interactions and transactions

    • Cleaning Up with Finally

  • The role of the 'finally' block

    • Use cases for 'finally'

    • Combining 'try', 'except', and 'finally'

    • Advanced Error Handling Techniques

  • Handling exceptions in loops

    • Context managers and the 'with' statement

    • Error handling in asynchronous code

Understanding Python Exceptions

Introduction to Exceptions

In Python, Exception is a fundamental aspect of the language. They are events that occur during the execution of a program that disrupts the normal flow of the program's instructions. When a Python script encounters a situation that it cannot cope with, it raises an exception. An exception in a more technical way is a Python object that represents an error.

While it might seem counterintuitive to consider error-producing mechanisms as a feature, exceptions play a crucial role in robust software tools. They don't cause a crash with a traceback (a sequence of text indicating the error's origin and endpoint). Instead, exceptions aid in decision-making by generating descriptive error messages, enabling you to handle both anticipated and unforeseen issues effectively.

For example, you want to get the division of two numbers. The algorithm of the code will go like this:

# Code that may raise an exception
x = 1
y = 0
z = x / y
print("The operation x/y equals:", z)

Output with a traceback:

Traceback (most recent call last):
  File "c:\Users\sample.py", line 4, in <module>
    z = x / y
        ~~^~~
ZeroDivisionError: division by zero

That output is a Python traceback, which is a report containing the function calls made in your code at a specific point. Here are the key parts:

  • Traceback (most recent call last): This line signifies the start of the traceback.

  • File "c:\Users\sample.py", line 4, in <module>: This line tells you the file in which the error occurred (sample.py), the line number where the error occurred (line 4), and the scope where the error occurred (<module>) - more like say a file that contains functions and variables.

  • z = x / y: This is the line of code that caused the error.

  • ZeroDivisionError: division by zero: This is the type of error that occurred (ZeroDivisionError), along with a message that gives more information about the error (division by zero).

Putting all these key parts together, the traceback is telling you that a ZeroDivisionError occurred on line 4 of the file sample.py, on the code z = x / y, because you tried to divide a number by zero, which is mathematically undefined. This information can be used to debug and fix the error.

Now that's a lot of info to take in

Mind blown

We have two classes of Exceptions in Python:

  • Built-in Exceptions and

  • user-defined Exceptions

In this guide, we'll focus more on built-in exceptions, and there will be an entirely different blog post on User-defined exceptions so we can have a clear margin there.

Common Types of Built-in Exceptions

Python has numerous built-in exceptions that force your program to output an error when something in the program goes wrong. Some common types of exceptions include: ImportError (when an import statement fails) IndexError (when a sequence subscript is out of range) TypeError (when an operation or function is applied to an object of inappropriate type) ValueError (when a built-in operation or function receives an argument that has the right type but an inappropriate value).

To see a list of other exceptions in python, follow this link to the Python documentation website for an extensive read of Python Built-in Exceptions

The Try-Except Block

Now, we know what an exception is in Python and how to break down the info to a more comprehensive level. But, to think about it, it is a very tedious process to go through each line to detect what the problem is. This is why Python provides a rich feature to help write codes better by catching errors in time.

The try and except statements in Python are used to catch and handle exceptions(which we know in our layman's term is an error). Python executes code following the try statement as a "normal" part of the program. The code that follows the except statement is the program's response to any exceptions in the preceding try clause.

Here's a simple implementation from the previous example when attempting to divide by zero:

try:
    # Code that may raise an exception
    x = 1
    y = 0
    z = x / y
except ZeroDivisionError:
    # What to do when the exception is raised
    print("You can't divide by zero!")

Output:

You can't divide by zero!

In this example, the code within the try block is executed line by line. If at any point an exception is raised, the execution stops and the code within the except block is run. If no exception is raised, the except block is skipped. And since an exception is raised, it runs the code in the except block.

Now, that's a big chess move

Mind at Ease

Syntax and Structure

Python's exception-handling mechanism is built around the try/except block. The syntax is as follows:

try:
    # Code that may raise an exception
except ExceptionType:
    # Code to execute if an exception of type ExceptionType is raised

Here, ExceptionType is the type of exception that you want to catch. If an exception of this type (or of a derived type) is raised inside the try block, the code inside the except block will be executed.

Handling Specific Exceptions

You can catch specific exceptions by specifying their type in the except clause. For example, to catch a ZeroDivisionError, you can do:

try:
    x = 1
    y = 0
    z = x / y
except ZeroDivisionError:
    print("You can't divide by zero!")

Output:

You can't divide by zero!

Multiple Except Blocks

You can have multiple except blocks to handle different types of exceptions separately. For example:

try:
    # Some code...
except ZeroDivisionError:
    print("You can't divide by zero!")
except TypeError:
    print("Wrong type!")

In this case, if a ZeroDivisionError is raised, the first except block will be executed. If a TypeError is raised, and the second except block will be executed.

Implementation:

try:
    x = 1
    y = "2"
    z = x / y
except ZeroDivisionError:
    print("You can't divide by zero!")
except TypeError:
    print("Wrong type!")

Output:

Wrong type!

In this example, we're trying to divide an integer (x) by a string (y), which is not allowed in Python and raises a TypeError. The except block for TypeError catches this exception and prints "Wrong type!". If y was 0, it would raise a ZeroDivisionError and the output would be "You can't divide by zero!".

Using the Else Clause

The else clause in a try/except block is used to specify a block of code to be executed if no exceptions are raised in the try block. For example:

try:
    x = 1
    y = 2
    z = x / y
except ZeroDivisionError:
    print("You can't divide by zero!")
else:
    print("The division was successful!")

Output:

The division was successful!

Raising Exceptions

You can manually raise exceptions using the raise statement. This is useful when you want to trigger an exception if a certain condition is met. For example:

x = 10
if x > 5:
    raise Exception('x should not exceed 5.')

Output:

Exception: x should not exceed 5.

The 'raise' statement

In Python, you can manually trigger exceptions using the raise statement. This is useful when you want to indicate that an error has occurred, just like in the raise implementation from the last section:

x = 10
if x > 5:
    raise Exception('x should not exceed 5.')

Output:

Exception: x should not exceed 5.

Creating custom exceptions

You can create your own exceptions in Python by creating a new exception class. This class should derive from the built-in Exception class. Here's an example:

class CustomError(Exception):
    pass

try:
    raise CustomError("This is a custom exception")
except CustomError as e:
    print(e)

Output:

Exception: This is a custom exception.

Best practices for raising exceptions

When raising exceptions, it's important to provide meaningful error messages so that the person reading the traceback can understand what went wrong. Also, it's a good practice to create and use custom exceptions when you need to raise an exception that doesn't fit into any of the built-in exception categories.

Exception Handling in Real-World Scenarios

In real-world scenarios, exception handling is crucial for building robust programs. Unhandled exceptions can cause your program to crash. By handling exceptions, you can ensure that your program can recover from errors and continue running. Here's an example of how you might handle exceptions in a real-world scenario:

try:
    # some code that might raise an exception
    file = open('non_existent_file.txt', 'r')
except FileNotFoundError:
    print("The file does not exist.")

Output:

Exception: The file does not exist

When you run this code, it will output: "The file does not exist." because the file 'non_existent_file.txt' does not exist. This demonstrates how you can handle exceptions to prevent your program from crashing when an error occurs.

File I/O and Exception Handling

File input/output (I/O) operations are a common source of errors in programming. Python provides built-in functions for file I/O, such as open(), read(), and write(). These functions can raise exceptions, such as FileNotFoundError(in the case where the file is not created yet and you want to read form it) or PermissionError(when the file is set to only be reable or writable or executable only), if something goes wrong.

-rw-r--r-- 1 ahmaddev 197121   35 Aug 30 07:54 online.txt
-rw-r--r-- 1 ahmaddev 197121   98 Nov  7 16:09 sample.py
-rw-r--r-- 1 ahmaddev 197121  118 Aug 30 07:28 sampletext.txt
-rw-r--r-- 1 ahmaddev 197121  475 Oct  8 08:19 test.py

In the file directory above:

-rw-r--r-- 1 Test 197121  475 Oct  8 08:19 test.py

The file permission is divided into three parts in the format "-rw-r--r--" which stands for:

  • owner (-rw-): This means that the owner, who in this case is Test has a read-and-write access (can view and change the content of the file - copy from and to)

  • group (r--): any group who has access to the directory will only have write only access (which means they can't change anything in the file, but can view the content of the file and copy from it)

  • others (r--): Other people who has access to the directory apart from the owner and group has only a read only access, just like in the second bullet point

'r' represents read access, 'w' represents write access and 'x' represents an executable access. The topic of file manipulation and permission is a broader scope in DevOps itself. So we'll reduce ourselves to the scope of this guide.

To handle these exceptions, you can use a try/except block. For example:

try:
    file = open('non_existent_file.txt', 'r')
except FileNotFoundError:
    print("The file does not exist.")

Network Operations and Error Management

Network operations, such as sending a request to a server or receiving data from a server, can also raise exceptions. For example, the requests library in Python raises a requests.exceptions.RequestException if a network request fails. You can catch and handle this exception like this:

import requests

try:
    response = requests.get('https://non_existent_website.com')
except requests.exceptions.RequestException:
    print("The request failed.")

Database Interactions and Transactions

When interacting with a database, you might encounter exceptions related to the database connection, the SQL queries, or the data itself. Most database libraries in Python provide their own set of exceptions that you can catch and handle. For example, the sqlite3 library raises a sqlite3.Error if a database operation fails:

import sqlite3

try:
    conn = sqlite3.connect('non_existent_database.db')
except sqlite3.Error:
    print("The database operation failed.")

Cleaning Up with Finally

The finally clause in a try/except block is used to specify a block of code that will be executed no matter whether an exception is raised or not. This is often used for cleanup actions that must always be completed, such as closing a file or a database connection:

try:
    file = open('file.txt', 'r')
    # Some code...
finally:
    file.close()

In this example, the file.close() statement will be executed even if an exception is raised inside the try block. This ensures that the file is properly closed even if an error occurs.

The Role of the 'Finally' Block

The finally block in Python is part of the try/except statement, which is used for exception handling. The finally block contains code that is always executed, whether an exception is raised or not. This is often used for cleanup actions, such as closing a file or a database connection.

Here's an example:

try:
    file = open('file.txt', 'r')
    # Some code...
finally:
    file.close()

When you run this code, it will always close the file, even if an error occurs in the try block.

Use Cases for 'Finally'

The finally block is useful in scenarios where certain code must be executed regardless of whether an error occurred or not. This is often the case for cleanup actions, such as:

  • Closing files or network connections.

  • Releasing resources, such as memory or hardware.

  • Restoring the state of the program or the system.

Combining 'Try', 'Except', and 'Finally'

You can combine try, except, and finally in a single statement to handle exceptions, execute certain code regardless of whether an exception occurred, and specify different actions for different types of exceptions. Here's an example:

try:
    x = 1
    y = 0
    z = x / y
except ZeroDivisionError:
    print("You can't divide by zero!")
finally:
    print("This is always executed.")

When you run this code, it will output:

You can't divide by zero!
This is always executed.

Advanced Error Handling Techniques

Python provides several advanced techniques for error handling, such as:

  • Chaining exceptions: You can raise a new exception while preserving the original traceback.

  • Creating custom exceptions: You can create your own exception types to represent specific errors in your program.

  • Using the else clause: You can use the else clause in a try/except statement to specify a block of code to be executed if no exceptions are raised.

Handling Exceptions in Loops

When an exception is raised inside a loop, it interrupts the loop. However, you can catch the exception and continue with the next iteration of the loop. Here's an example:

for i in range(5):
    try:
        if i == 2:
            raise Exception("An error occurred!")
        else:
            print(i)
    except Exception:
        continue

When you run this code, it will output:

0
1
3
4

Context Managers and the 'With' Statement

A context manager is an object that defines methods to be used in conjunction with the with statement, including methods to handle setup and teardown actions. When used with file operations, it can help ensure that files are properly closed after use, even if errors occur. Here's an example:

try:
    with open('file.txt', 'r') as file:
        # Some code...
except FileNotFoundError:
    print("The file does not exist.")

In this example, the with statement automatically closes the file after the nested block of code is executed, even if an exception is raised inside the block.

Error Handling in Asynchronous Code

Asynchronous code can also raise exceptions, and these exceptions can be caught and handled just like in synchronous code. However, because asynchronous code can have multiple execution paths running concurrently, exceptions may need to be handled in each execution path separately. This can be done using try/except blocks inside asynchronous functions or coroutines. But we won't go into the implementation details since it goes beyond the scope of this topic.

You've come to the end of this Guide!!!

End of post appreciation

If you enjoyed this guide, do well to stay updated on our latest Technical content and installation guides. You can follow the Technical writer on Twitter, LinkedIn, Dev.to, Medium