Menu iconMenu iconPython & SQL Bible
Python & SQL Bible

Chapter 8: Exceptional Python

8.1 Error and Exception Handling

This chapter provides a detailed overview of Python's system for handling unexpected events through the use of exceptions. Exceptions are a key component of any robust software application as they enable the program to gracefully recover from errors and continue functioning properly.

In order to fully grasp the concept of exceptions, we will explore the various ways in which Python allows us to interact with them, including how to handle exceptions and even create our own custom exceptions. We will delve into the importance of proper exception handling in software development, examining real-world examples and best practices for implementing effective exception handling strategies.

By the end of this chapter, you will have a solid understanding of how exceptions work in Python and how to leverage their power to create more reliable and resilient software applications.

In the field of programming, it is often said that mistakes are inevitable. It is important to note that there are two main types of errors that programmers must be aware of: syntax errors and exceptions. Syntax errors, also known as parsing errors, occur when the code contains an incorrect statement that is not in accordance with the rules of the programming language. These errors are detected by the parser during the process of code compilation.

Meanwhile, exceptions are another type of error that can occur during program execution. These errors are detected by the system in real-time as the code is being executed. Exceptions can occur for a variety of reasons, such as when a program tries to access a file that does not exist or when it attempts to divide a number by zero. It is important for programmers to be able to identify and handle exceptions properly in order to ensure that their programs run smoothly and without issue. By using try-catch blocks, programmers can anticipate and respond to exceptions in a way that minimizes the impact on the overall program.

Here's a basic example:

print(0 / 0)

When we run this code, we get a ZeroDivisionError:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

ZeroDivisionError is an exception in Python, raised when we try to divide a number by zero. When an error like this occurs and is not handled by the program, it halts execution and shows a traceback to the console, which can help developers understand what went wrong.

However, stopping program execution is not always the desired outcome. Sometimes, we want our program to continue running even if some part of it encounters an error. To do this, we need to handle the exceptions that might occur. Python uses a try/except block to handle exceptions.

Here's an example:

try:
    print(0 / 0)
except ZeroDivisionError:
    print("You can't divide by zero!")

Now, instead of stopping the program and printing a traceback, we print "You can't divide by zero!" and the program continues running.

It's also important to note that Python allows handling multiple exceptions. If you have code that might raise more than one type of exception, you can include a tuple of the exceptions you want to catch.

For example:

try:
    # some code here
except (TypeError, ValueError) as e:
    print("Caught an exception:", e)

In the example above, the try block will catch either a TypeError or a ValueError. If any other type of exception is thrown, it will not be caught by this except block.

Python also allows us to capture the error message of an exception using the as keyword. The variable following as in the except clause is assigned the exception instance. This instance has a .__str__() method which can be used to display a more human-readable explanation of the error.

Furthermore, Python also includes else and finally clauses in its exception handling, which we'll explore in detail in the coming sections. The else clause is used to check if the try block did not raise any exceptions, and the finally clause is used to specify a block of code to be executed no matter what, whether an exception was raised or not.

Now, tet's continue with the else and finally clauses in Python's error handling mechanism.

8.1.1 Else Clause

In Python, try and except statements are used to handle exceptions that may occur during the execution of a program. The try block contains the code that may raise an exception, while the except block contains the code that will be executed if an exception is raised. However, there is an optional clause called else that can be used in conjunction with the try and except statements.

The else clause is executed only if no exceptions are raised in the try block. It is often used to perform additional actions that should only occur if the code in the try block runs successfully. For example, if you are working with a file in Python and want to read its contents, you can use a try block to attempt to read the file. If the file does not exist or cannot be read, an exception will be raised and the code in the except block will be executed. However, if the file can be successfully read, you can use the else clause to perform additional actions, such as processing the file's contents.

In summary, the else clause is a useful addition to the try and except statements in Python, as it allows you to perform actions that should only occur if the code in the try block runs successfully, without cluttering up the try or except blocks.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
else:
    print("No exceptions were thrown.")

In the above example, if the code within the try block executes without raising any exceptions, the else block is executed, and "No exceptions were thrown." will be printed.

8.1.2 Finally Clause

Python's finally clause is a crucial part of exception handling. It can be used to specify a block of code that must be executed no matter what, whether an exception was raised or not. This can be especially useful for ensuring that cleanup activities, like closing files or network connections, are performed properly. Without a finally clause, these cleanup activities may not get executed if an exception occurs, which can lead to resource leaks or other issues.

In addition to its use in cleanup activities, the finally clause can also be used for other purposes. For example, it can be used to ensure that certain code is always executed, regardless of whether an exception was raised or not. This can be useful in situations where you need to perform some action, but also want to handle any exceptions that might occur.

Overall, the finally clause is a powerful tool for ensuring that your code behaves correctly in the face of exceptions. By using it properly, you can ensure that your code always executes the necessary cleanup activities, and that it handles exceptions in a robust and reliable manner.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
finally:
    print("This will always run.")

In the example above, no matter what happens in the try block and except block, the finally block will always run and "This will always run." will be printed.

By understanding and using these clauses, you can create robust Python code that anticipates and handles errors gracefully while also ensuring necessary cleanup actions are performed. This is essential for maintaining the health and stability of your software applications.

8.1.3 Custom Exceptions

In creating custom exceptions in Python, it is important to note that these exceptions should be specific to your application's domain. This means that you should consider the kinds of errors that may occur in your application and create exceptions that can handle these errors accordingly.

To create custom exceptions, you need to create new exception classes that are derived from the built-in Exception class in Python. You can either derive your custom exception class directly from the Exception class, or indirectly from any of the other built-in exception classes in Python.

Once you have created your custom exception classes, you can then use them in your application to handle specific errors and exceptions that may occur. By doing so, you can ensure that your application is more robust and can handle a wider range of errors and exceptions that may occur during execution.

Example:

pythonCopy code
class CustomError(Exception):
    pass

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

In the above example, we first define a new exception class called CustomError that inherits from Exception. We can then raise our custom exception using the raise statement and catch it using an except block.

Creating custom exceptions can make your code more expressive and easier to debug, since you can create specific exceptions for different error conditions in your application.

8.1 Error and Exception Handling

This chapter provides a detailed overview of Python's system for handling unexpected events through the use of exceptions. Exceptions are a key component of any robust software application as they enable the program to gracefully recover from errors and continue functioning properly.

In order to fully grasp the concept of exceptions, we will explore the various ways in which Python allows us to interact with them, including how to handle exceptions and even create our own custom exceptions. We will delve into the importance of proper exception handling in software development, examining real-world examples and best practices for implementing effective exception handling strategies.

By the end of this chapter, you will have a solid understanding of how exceptions work in Python and how to leverage their power to create more reliable and resilient software applications.

In the field of programming, it is often said that mistakes are inevitable. It is important to note that there are two main types of errors that programmers must be aware of: syntax errors and exceptions. Syntax errors, also known as parsing errors, occur when the code contains an incorrect statement that is not in accordance with the rules of the programming language. These errors are detected by the parser during the process of code compilation.

Meanwhile, exceptions are another type of error that can occur during program execution. These errors are detected by the system in real-time as the code is being executed. Exceptions can occur for a variety of reasons, such as when a program tries to access a file that does not exist or when it attempts to divide a number by zero. It is important for programmers to be able to identify and handle exceptions properly in order to ensure that their programs run smoothly and without issue. By using try-catch blocks, programmers can anticipate and respond to exceptions in a way that minimizes the impact on the overall program.

Here's a basic example:

print(0 / 0)

When we run this code, we get a ZeroDivisionError:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

ZeroDivisionError is an exception in Python, raised when we try to divide a number by zero. When an error like this occurs and is not handled by the program, it halts execution and shows a traceback to the console, which can help developers understand what went wrong.

However, stopping program execution is not always the desired outcome. Sometimes, we want our program to continue running even if some part of it encounters an error. To do this, we need to handle the exceptions that might occur. Python uses a try/except block to handle exceptions.

Here's an example:

try:
    print(0 / 0)
except ZeroDivisionError:
    print("You can't divide by zero!")

Now, instead of stopping the program and printing a traceback, we print "You can't divide by zero!" and the program continues running.

It's also important to note that Python allows handling multiple exceptions. If you have code that might raise more than one type of exception, you can include a tuple of the exceptions you want to catch.

For example:

try:
    # some code here
except (TypeError, ValueError) as e:
    print("Caught an exception:", e)

In the example above, the try block will catch either a TypeError or a ValueError. If any other type of exception is thrown, it will not be caught by this except block.

Python also allows us to capture the error message of an exception using the as keyword. The variable following as in the except clause is assigned the exception instance. This instance has a .__str__() method which can be used to display a more human-readable explanation of the error.

Furthermore, Python also includes else and finally clauses in its exception handling, which we'll explore in detail in the coming sections. The else clause is used to check if the try block did not raise any exceptions, and the finally clause is used to specify a block of code to be executed no matter what, whether an exception was raised or not.

Now, tet's continue with the else and finally clauses in Python's error handling mechanism.

8.1.1 Else Clause

In Python, try and except statements are used to handle exceptions that may occur during the execution of a program. The try block contains the code that may raise an exception, while the except block contains the code that will be executed if an exception is raised. However, there is an optional clause called else that can be used in conjunction with the try and except statements.

The else clause is executed only if no exceptions are raised in the try block. It is often used to perform additional actions that should only occur if the code in the try block runs successfully. For example, if you are working with a file in Python and want to read its contents, you can use a try block to attempt to read the file. If the file does not exist or cannot be read, an exception will be raised and the code in the except block will be executed. However, if the file can be successfully read, you can use the else clause to perform additional actions, such as processing the file's contents.

In summary, the else clause is a useful addition to the try and except statements in Python, as it allows you to perform actions that should only occur if the code in the try block runs successfully, without cluttering up the try or except blocks.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
else:
    print("No exceptions were thrown.")

In the above example, if the code within the try block executes without raising any exceptions, the else block is executed, and "No exceptions were thrown." will be printed.

8.1.2 Finally Clause

Python's finally clause is a crucial part of exception handling. It can be used to specify a block of code that must be executed no matter what, whether an exception was raised or not. This can be especially useful for ensuring that cleanup activities, like closing files or network connections, are performed properly. Without a finally clause, these cleanup activities may not get executed if an exception occurs, which can lead to resource leaks or other issues.

In addition to its use in cleanup activities, the finally clause can also be used for other purposes. For example, it can be used to ensure that certain code is always executed, regardless of whether an exception was raised or not. This can be useful in situations where you need to perform some action, but also want to handle any exceptions that might occur.

Overall, the finally clause is a powerful tool for ensuring that your code behaves correctly in the face of exceptions. By using it properly, you can ensure that your code always executes the necessary cleanup activities, and that it handles exceptions in a robust and reliable manner.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
finally:
    print("This will always run.")

In the example above, no matter what happens in the try block and except block, the finally block will always run and "This will always run." will be printed.

By understanding and using these clauses, you can create robust Python code that anticipates and handles errors gracefully while also ensuring necessary cleanup actions are performed. This is essential for maintaining the health and stability of your software applications.

8.1.3 Custom Exceptions

In creating custom exceptions in Python, it is important to note that these exceptions should be specific to your application's domain. This means that you should consider the kinds of errors that may occur in your application and create exceptions that can handle these errors accordingly.

To create custom exceptions, you need to create new exception classes that are derived from the built-in Exception class in Python. You can either derive your custom exception class directly from the Exception class, or indirectly from any of the other built-in exception classes in Python.

Once you have created your custom exception classes, you can then use them in your application to handle specific errors and exceptions that may occur. By doing so, you can ensure that your application is more robust and can handle a wider range of errors and exceptions that may occur during execution.

Example:

pythonCopy code
class CustomError(Exception):
    pass

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

In the above example, we first define a new exception class called CustomError that inherits from Exception. We can then raise our custom exception using the raise statement and catch it using an except block.

Creating custom exceptions can make your code more expressive and easier to debug, since you can create specific exceptions for different error conditions in your application.

8.1 Error and Exception Handling

This chapter provides a detailed overview of Python's system for handling unexpected events through the use of exceptions. Exceptions are a key component of any robust software application as they enable the program to gracefully recover from errors and continue functioning properly.

In order to fully grasp the concept of exceptions, we will explore the various ways in which Python allows us to interact with them, including how to handle exceptions and even create our own custom exceptions. We will delve into the importance of proper exception handling in software development, examining real-world examples and best practices for implementing effective exception handling strategies.

By the end of this chapter, you will have a solid understanding of how exceptions work in Python and how to leverage their power to create more reliable and resilient software applications.

In the field of programming, it is often said that mistakes are inevitable. It is important to note that there are two main types of errors that programmers must be aware of: syntax errors and exceptions. Syntax errors, also known as parsing errors, occur when the code contains an incorrect statement that is not in accordance with the rules of the programming language. These errors are detected by the parser during the process of code compilation.

Meanwhile, exceptions are another type of error that can occur during program execution. These errors are detected by the system in real-time as the code is being executed. Exceptions can occur for a variety of reasons, such as when a program tries to access a file that does not exist or when it attempts to divide a number by zero. It is important for programmers to be able to identify and handle exceptions properly in order to ensure that their programs run smoothly and without issue. By using try-catch blocks, programmers can anticipate and respond to exceptions in a way that minimizes the impact on the overall program.

Here's a basic example:

print(0 / 0)

When we run this code, we get a ZeroDivisionError:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

ZeroDivisionError is an exception in Python, raised when we try to divide a number by zero. When an error like this occurs and is not handled by the program, it halts execution and shows a traceback to the console, which can help developers understand what went wrong.

However, stopping program execution is not always the desired outcome. Sometimes, we want our program to continue running even if some part of it encounters an error. To do this, we need to handle the exceptions that might occur. Python uses a try/except block to handle exceptions.

Here's an example:

try:
    print(0 / 0)
except ZeroDivisionError:
    print("You can't divide by zero!")

Now, instead of stopping the program and printing a traceback, we print "You can't divide by zero!" and the program continues running.

It's also important to note that Python allows handling multiple exceptions. If you have code that might raise more than one type of exception, you can include a tuple of the exceptions you want to catch.

For example:

try:
    # some code here
except (TypeError, ValueError) as e:
    print("Caught an exception:", e)

In the example above, the try block will catch either a TypeError or a ValueError. If any other type of exception is thrown, it will not be caught by this except block.

Python also allows us to capture the error message of an exception using the as keyword. The variable following as in the except clause is assigned the exception instance. This instance has a .__str__() method which can be used to display a more human-readable explanation of the error.

Furthermore, Python also includes else and finally clauses in its exception handling, which we'll explore in detail in the coming sections. The else clause is used to check if the try block did not raise any exceptions, and the finally clause is used to specify a block of code to be executed no matter what, whether an exception was raised or not.

Now, tet's continue with the else and finally clauses in Python's error handling mechanism.

8.1.1 Else Clause

In Python, try and except statements are used to handle exceptions that may occur during the execution of a program. The try block contains the code that may raise an exception, while the except block contains the code that will be executed if an exception is raised. However, there is an optional clause called else that can be used in conjunction with the try and except statements.

The else clause is executed only if no exceptions are raised in the try block. It is often used to perform additional actions that should only occur if the code in the try block runs successfully. For example, if you are working with a file in Python and want to read its contents, you can use a try block to attempt to read the file. If the file does not exist or cannot be read, an exception will be raised and the code in the except block will be executed. However, if the file can be successfully read, you can use the else clause to perform additional actions, such as processing the file's contents.

In summary, the else clause is a useful addition to the try and except statements in Python, as it allows you to perform actions that should only occur if the code in the try block runs successfully, without cluttering up the try or except blocks.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
else:
    print("No exceptions were thrown.")

In the above example, if the code within the try block executes without raising any exceptions, the else block is executed, and "No exceptions were thrown." will be printed.

8.1.2 Finally Clause

Python's finally clause is a crucial part of exception handling. It can be used to specify a block of code that must be executed no matter what, whether an exception was raised or not. This can be especially useful for ensuring that cleanup activities, like closing files or network connections, are performed properly. Without a finally clause, these cleanup activities may not get executed if an exception occurs, which can lead to resource leaks or other issues.

In addition to its use in cleanup activities, the finally clause can also be used for other purposes. For example, it can be used to ensure that certain code is always executed, regardless of whether an exception was raised or not. This can be useful in situations where you need to perform some action, but also want to handle any exceptions that might occur.

Overall, the finally clause is a powerful tool for ensuring that your code behaves correctly in the face of exceptions. By using it properly, you can ensure that your code always executes the necessary cleanup activities, and that it handles exceptions in a robust and reliable manner.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
finally:
    print("This will always run.")

In the example above, no matter what happens in the try block and except block, the finally block will always run and "This will always run." will be printed.

By understanding and using these clauses, you can create robust Python code that anticipates and handles errors gracefully while also ensuring necessary cleanup actions are performed. This is essential for maintaining the health and stability of your software applications.

8.1.3 Custom Exceptions

In creating custom exceptions in Python, it is important to note that these exceptions should be specific to your application's domain. This means that you should consider the kinds of errors that may occur in your application and create exceptions that can handle these errors accordingly.

To create custom exceptions, you need to create new exception classes that are derived from the built-in Exception class in Python. You can either derive your custom exception class directly from the Exception class, or indirectly from any of the other built-in exception classes in Python.

Once you have created your custom exception classes, you can then use them in your application to handle specific errors and exceptions that may occur. By doing so, you can ensure that your application is more robust and can handle a wider range of errors and exceptions that may occur during execution.

Example:

pythonCopy code
class CustomError(Exception):
    pass

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

In the above example, we first define a new exception class called CustomError that inherits from Exception. We can then raise our custom exception using the raise statement and catch it using an except block.

Creating custom exceptions can make your code more expressive and easier to debug, since you can create specific exceptions for different error conditions in your application.

8.1 Error and Exception Handling

This chapter provides a detailed overview of Python's system for handling unexpected events through the use of exceptions. Exceptions are a key component of any robust software application as they enable the program to gracefully recover from errors and continue functioning properly.

In order to fully grasp the concept of exceptions, we will explore the various ways in which Python allows us to interact with them, including how to handle exceptions and even create our own custom exceptions. We will delve into the importance of proper exception handling in software development, examining real-world examples and best practices for implementing effective exception handling strategies.

By the end of this chapter, you will have a solid understanding of how exceptions work in Python and how to leverage their power to create more reliable and resilient software applications.

In the field of programming, it is often said that mistakes are inevitable. It is important to note that there are two main types of errors that programmers must be aware of: syntax errors and exceptions. Syntax errors, also known as parsing errors, occur when the code contains an incorrect statement that is not in accordance with the rules of the programming language. These errors are detected by the parser during the process of code compilation.

Meanwhile, exceptions are another type of error that can occur during program execution. These errors are detected by the system in real-time as the code is being executed. Exceptions can occur for a variety of reasons, such as when a program tries to access a file that does not exist or when it attempts to divide a number by zero. It is important for programmers to be able to identify and handle exceptions properly in order to ensure that their programs run smoothly and without issue. By using try-catch blocks, programmers can anticipate and respond to exceptions in a way that minimizes the impact on the overall program.

Here's a basic example:

print(0 / 0)

When we run this code, we get a ZeroDivisionError:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

ZeroDivisionError is an exception in Python, raised when we try to divide a number by zero. When an error like this occurs and is not handled by the program, it halts execution and shows a traceback to the console, which can help developers understand what went wrong.

However, stopping program execution is not always the desired outcome. Sometimes, we want our program to continue running even if some part of it encounters an error. To do this, we need to handle the exceptions that might occur. Python uses a try/except block to handle exceptions.

Here's an example:

try:
    print(0 / 0)
except ZeroDivisionError:
    print("You can't divide by zero!")

Now, instead of stopping the program and printing a traceback, we print "You can't divide by zero!" and the program continues running.

It's also important to note that Python allows handling multiple exceptions. If you have code that might raise more than one type of exception, you can include a tuple of the exceptions you want to catch.

For example:

try:
    # some code here
except (TypeError, ValueError) as e:
    print("Caught an exception:", e)

In the example above, the try block will catch either a TypeError or a ValueError. If any other type of exception is thrown, it will not be caught by this except block.

Python also allows us to capture the error message of an exception using the as keyword. The variable following as in the except clause is assigned the exception instance. This instance has a .__str__() method which can be used to display a more human-readable explanation of the error.

Furthermore, Python also includes else and finally clauses in its exception handling, which we'll explore in detail in the coming sections. The else clause is used to check if the try block did not raise any exceptions, and the finally clause is used to specify a block of code to be executed no matter what, whether an exception was raised or not.

Now, tet's continue with the else and finally clauses in Python's error handling mechanism.

8.1.1 Else Clause

In Python, try and except statements are used to handle exceptions that may occur during the execution of a program. The try block contains the code that may raise an exception, while the except block contains the code that will be executed if an exception is raised. However, there is an optional clause called else that can be used in conjunction with the try and except statements.

The else clause is executed only if no exceptions are raised in the try block. It is often used to perform additional actions that should only occur if the code in the try block runs successfully. For example, if you are working with a file in Python and want to read its contents, you can use a try block to attempt to read the file. If the file does not exist or cannot be read, an exception will be raised and the code in the except block will be executed. However, if the file can be successfully read, you can use the else clause to perform additional actions, such as processing the file's contents.

In summary, the else clause is a useful addition to the try and except statements in Python, as it allows you to perform actions that should only occur if the code in the try block runs successfully, without cluttering up the try or except blocks.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
else:
    print("No exceptions were thrown.")

In the above example, if the code within the try block executes without raising any exceptions, the else block is executed, and "No exceptions were thrown." will be printed.

8.1.2 Finally Clause

Python's finally clause is a crucial part of exception handling. It can be used to specify a block of code that must be executed no matter what, whether an exception was raised or not. This can be especially useful for ensuring that cleanup activities, like closing files or network connections, are performed properly. Without a finally clause, these cleanup activities may not get executed if an exception occurs, which can lead to resource leaks or other issues.

In addition to its use in cleanup activities, the finally clause can also be used for other purposes. For example, it can be used to ensure that certain code is always executed, regardless of whether an exception was raised or not. This can be useful in situations where you need to perform some action, but also want to handle any exceptions that might occur.

Overall, the finally clause is a powerful tool for ensuring that your code behaves correctly in the face of exceptions. By using it properly, you can ensure that your code always executes the necessary cleanup activities, and that it handles exceptions in a robust and reliable manner.

Example:

try:
    # Some code here
except Exception as e:
    print("Caught an exception:", e)
finally:
    print("This will always run.")

In the example above, no matter what happens in the try block and except block, the finally block will always run and "This will always run." will be printed.

By understanding and using these clauses, you can create robust Python code that anticipates and handles errors gracefully while also ensuring necessary cleanup actions are performed. This is essential for maintaining the health and stability of your software applications.

8.1.3 Custom Exceptions

In creating custom exceptions in Python, it is important to note that these exceptions should be specific to your application's domain. This means that you should consider the kinds of errors that may occur in your application and create exceptions that can handle these errors accordingly.

To create custom exceptions, you need to create new exception classes that are derived from the built-in Exception class in Python. You can either derive your custom exception class directly from the Exception class, or indirectly from any of the other built-in exception classes in Python.

Once you have created your custom exception classes, you can then use them in your application to handle specific errors and exceptions that may occur. By doing so, you can ensure that your application is more robust and can handle a wider range of errors and exceptions that may occur during execution.

Example:

pythonCopy code
class CustomError(Exception):
    pass

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

In the above example, we first define a new exception class called CustomError that inherits from Exception. We can then raise our custom exception using the raise statement and catch it using an except block.

Creating custom exceptions can make your code more expressive and easier to debug, since you can create specific exceptions for different error conditions in your application.