Python Exception Handling and Customization
Tags: python
Published: 2023-09-17 23:47:05


---
canonical_url: https://erikscode.space/index.php/2023/09/17/python-exception-handling-and-customization/
cover_image: https://i0.wp.com/erikscode.space/wp-content/uploads/2023/09/pexels-photo-7140251.jpeg?fit=1880%2C1253&ssl=1
---

Like bugs, exceptions are inevitable when developing software, especially as the complexity of that software increases. Sometimes exceptions are surprising, other times we can anticipate them coming. How a program responds to the occurrence of exceptions is called exception handling, and as programmers, we can define and customize exception handling. In this chapter, we’ll learn what exceptions are, how to handle them, and how to make our own.

What are Exceptions?
--------------------

Exceptions can be thought of as unplanned events in the execution of a program that disrupt that execution. When a runtime error occurs, a specific exception is raised. If an exception is raised and the program does not have any code defining how to handle that exception, the exception is said to be uncaught, and it will terminate execution of the program. Exceptions are not unique to Python; every programming language has exceptions and a means of handling them. You’ve actually most likely seen lots of exceptions already, but just for clarity, let’s raise a few exceptions on purpose.

First, in a Python console, try to divide any number by zero and press enter. You should see something similar to the following:

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


When we tried to divide by zero, Python raised an exception. In this case, the exception raised was `ZeroDivisionError`. `ZeroDivisionError` is the name of the exception raised when a program tries to divide by zero. There are other kinds of exceptions as well, let’s raise another. In the Python console, try to add a number to a string:

```
>>> print('hi' + 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
```


This time, Python raised a `TypeError`. `TypeError` is the kind of exception raised when a program tries to do something to an object that the object’s class doesn’t support. Notice also that there’s a message along with the exception: `can only concatenate str (not “in”) to str`. Exceptions can, and often do, have different messages for different ways in which they can be raised. For example, let’s raise another `TypeError`, this time by trying to divide a number by a string:

```
>>> print(20 / 'hi')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'int' and 'str'
```


Again, Python raises a `TypeError` in this situation, but notice that the message is different. The message says `unsupported operand type(s) for /: ‘int’ and ‘str’`. This will be relevant later when we write our own exceptions.

Now that we have an intuition about what exceptions are, lets learn how to handle them.

Handling Exceptions
-------------------

An exception will terminate the execution of a program if left unhandled. For example, consider the following script:

```python
def divide(x, y):
  return x / y

print(divide(4, 2))
print(divide(2, 0))
print(divide(9, 3))
```


In this script, we have a method called `divide` that takes two parameters, divides them, and returns the result. Next, we have three lines calling the `divide` function and printing the result. Notice that the second line passes zero as a second parameter; let’s see what happens when we try to run this script:

```
2.0
Traceback (most recent call last):
  File "path/to/script.py", line 5, in <module>
    print(divide(2, 0))
  File "path/to/script.py", line 2, in divide
    return x / y
ZeroDivisionError: division by zero
```


Notice that the program printed 2.0 to the console, meaning the first call to the `divide` function worked. Then, the program raised an exception, `ZeroDivisionError`, on the second line because we tried to divide by zero. Notice also that the program did not run the final line, `print(divide(9, 3))`. This is because the `ZeroDivisionError` was an uncaught exception. In order to make the program continue running, we have to write code specifically for handling that exception. We do this in Python with `try except` blocks.

### Using try and except

In Python, we use the keywords `try` and `except` to handle exceptions. The `try` block contains the operation that may raise an exception, and the `except` block contains the code that defines what we want the program to do when an exception is raised. As an example, let’s rewrite the `divide` function from earlier to handle `ZeroDivisionError` exceptions:

```python
def divide(x, y):
  try:
    return x / y
  except ZeroDivisionError:
     return "Cannot divide by zero"
```


Notice that the line `return x / y` is now in a `try` block. This tells Python to run the code in this block until the last line, or until an exception is raised. If an exception is raised, Python checks for an `except` block that matches the type of exception. In our case, we wrote except `ZeroDivisionError`, so if a `ZeroDivisionError` is raised, Python will run the code inside of the `except` block. To see this is action, run the script with the updated `divide` function. You should see the following output in the console:

```
2.0
Cannot divide by zero
3.0
```


This time, when the script got to the line that said `print(divide(2, 0))`, the program did not print the exception message to the console, it instead ran the code in the `except` block, which we can see in the console when it printed `Cannot divide by zero`. Notice also that even though an exception was raised, the program continued execution, as we can see by the 3.0 printed to the console when Python ran the last line of our script. This is an example of how exception handling is useful, we can anticipate problems and handle them without terminating the program.

Note: When a program encounters an error or otherwise fails in some way but does not terminate its execution, it is said to fail *gracefully*.

### Handling Other Types of Exceptions

The `divide` function can now fail gracefully when a `ZeroDivisionError` exception is raised, but what about other exceptions? What if someone tries to pass a string to the function? As an example, update the last lines in the previous script to look like the following:

```python
print(divide(4, 2))
print(divide(2, 0))
print(divide('hi', 2))
print(divide(9, 3))
```


If you try to run the script now, you’ll see the following output:

```
2.0
Cannot divide by zero
Traceback (most recent call last):
  File "path/to/script.py", line 14, in <module>
    print(divide('hi', 2))
  File "path/to/script.py", line 7, in divide
    return x / y
TypeError: unsupported operand type(s) for /: 'str' and 'int'
```


Notice this time that Python raised a `TypeError` when we tried to divide a string with an integer. We never wrote any code to handle this exception, so Python terminated the program once this exception was raised. If we want to add code to handle this exception, we have two options.

First, we could add another `except` block to the `divide` function for that specific exception. The function would then look like the following:

```python
def divide(x, y):
  try:
    return x / y
  except ZeroDivisionError:
     return "Cannot divide by zero"
  except TypeError:
    return "Can only divide number types"
```


This would handle both `ZeroDivisionError` and `TypeError` exceptions. But there are yet more exceptions, we cannot anticipate them all. Instead of writing an `except` block for each exception type that could possibly beraised, we can write an `except` block that catches all exceptions besides the `ZeroDivisionError` one. This approach would have the function looking like this:

```python
def divide(x, y):
  try:
    return x / y
  except ZeroDivisionError:
     return "Cannot divide by zero"
  except:
    return "An error occurred"
```


With the `divide` function written this way, there is no exception that can be raised which would terminate execution of our program. We have an `except` block dedicated specifically to catching `ZeroDivisionError` exceptions and informing the user they cannot divide by zero, and we have an `except` block that catches any other kind of exception that could possibly be raised and simply alerts the user that an error occurred. These are the basics of handling exceptions in Python but there’s one more thing to learn about, finally blocks.

### Using finally

When a line of code raises an exception, Python immediately leaves that line and goes to the except block (or terminates the program) meaning that nothing under the line raising the exception gets executed. To get an idea why this is a problem, consider the following script:

```python
def add(x, y):
  try:
    print(x + y)
    print("Add function completed")
  except:
    print("An error occurred")

add(2, 4)
add('hi', 9)
```


Here, we’ve defined an `add` function that tries to add its parameters together and print the result to the console, then prints that it has completed. An `except` block catches any exceptions and prints that an error has occurred. Under the function we call the function twice, once with numbers and once with a string and a number, forcing an exception. Let’s see what happens:

```
6
Add function completed
An error occurred
```


Notice that when the exception was raised, we didn’t see `Add function completed` printed to the console. This is because once the error was raised, the Python interpreter left the `try` block and went to the `except` block, leaving the `print` statement unexecuted. If we want to execute code regardless of if an error is raised or not, we have to use a `finally` block. The `finally` keyword is used like so:

```python
def add(x, y):
  try:
    print(x + y)
  except:
    print("An error occurred")
  finally:
    print("Add function completed")
```


We’ve added a `finally` block and put the line printing `Add function completed` in it. This will ensure that this line runs every time the function is called, regardless of whether there is a caught exception. Run the script again and you should see the following output:

```
6
Add function completed
An error occurred
Add function completed
```


This time, the message `Add function completed` prints to the console every time. This is how `finally` blocks work.

Now we know how to handle exceptions with `try`, `except`, and `finally`. There is one other thing you should know about exception handling before we move on to customized exceptions. Handling exceptions is computationally slow. This is for a variety of reasons that are much too technical to be relevant to this chapter. The main point is that using exception handling to handle errors may be slower than other techniques (like `if` statements). Exception handling is best used when we don’t want errors to terminate program execution. Otherwise, it’s often better to use `if` statements or to just let the program execution terminate. With that caution out of the way, lets talk about customized exceptions.

Customized Exceptions
---------------------

In the previous sections, we raised two different types of exceptions, `ZeroDivisionError` and `TypeError`. These exception classes are just two of many other built-in exception classes in Python. Other exception classes include `ImportError`, `ModuleNotFoundError`, `KeyError`, and many more. In addition to these built-in exception classes, we can also make our own custom exception classes. Customizing exception classes is useful in large projects because it aids in debugging, and it helps you define how errors are handled in your system.

### Creating a Custom Exception Class

To create a customized exception, we simply write a class that inherits Python’s `Exception` class. Write the following:

```python
class MyError(Exception):
  pass
```


As you can see, writing a custom exception is just a matter of making a subclass of `Exception`. Now, to raise this exception, we use the raise keyword. Consider the following script:

```python
for i in range(1, 5):
  print(i)
  if i == 3:
    raise MyError
```


In this script, we make a simple loop that prints its iteration number but raises our customized `MyError` exception if the number is 3. If you run this script, you should see the following output:

```
1
2
3
Traceback (most recent call last):
  File "path/to/script.py", line 8, in <module>
    raise MyError
__main__.MyError
```


Notice that in the output, we see that our custom exception was raised. Suppose we did not know about this script but found a bug report saying “some method is throwing a `MyError` exception.” Since this is a custom (that is, non-built-in) exception class, we can search our codebase for any line that says `raise MyError` and figure out what’s going on. When developing large and complex software systems, saving time by searching for customized exceptions is very helpful.

### Editing the Exception Message

We can further customize our exception classes by overriding their constructor methods and defining what message they print to the console. For example, suppose we are writing a human resources application for tracking employee salaries. We might have an `Employee` class like the following:

```python
class Employee:
  def __init__(self, name, salary):
    self.name = name
    self.salary = salary
```


Here we have a basic `Employee` class that takes a `name` and `salary` attribute in its constructor. Now suppose we wanted to ensure that an employee’s salary is between 20 thousand and 500 thousand and we want to not only raise an exception when someone attempts to make an Employee object with a `salary` attribute outside of that range, we also want to log such an event to a database. First, we have to create a custom exception, we’ll call it `SalaryError`. Then, we’ll override its constructor to print why an exception was raised and to log the exception to a database. Check it out:

```python
class SalaryError(Exception):
  def __init__(self, salary):
    self.message = f"Salary must be between 20k and 500k, you put {salary}"
    print("Logging the following to the database:")
    print(f"Attempted to create employee with salary {salary}")
    super().__init__(self.message)
```


In this code, we create a new class called `SalaryError` and inherit the `Exception` class, creating a custom exception. Then, we override the constructor by telling it to expect an input called `salary`. Then, we set the exception message to inform that a salary attribute must be between 20,000 and 500,000 and put what the attempted salary was.

Next, we print that we are logging the error to the database (in this example, we don’t actually log anything to a database since setting up the necessary connections for that would take away from the focus of this article). Finally, we use `super()` to fill out the base `Exception` class’s information so that the class will behave as a standard Python exception.

In order to implement this exception, we’ll tweak the `Employee` constructor to raise the exception if the `salary` attribute falls out of the desired range:

```python
class Employee:
  def __init__(self, name, salary):
    if salary < 20000 or salary > 500000:
      raise SalaryError(salary)
    self.name = name
    self.salary = salary
```


Here, we’ve rewritten the `Employee` constructor to check if the `salary` range is within the 20 to 500 thousand range. If it is, we continue with constructing the object; if not, we raise the `SalaryError` exception. To see how this works, write the following code:

```python
bob = Employee('Bob', 19000)
```


This code tries to instantiate an `Employee` object with a `salary` of 19,000, just below the required range. If you try to run this code, you should see the following output in the console:

```
Logging the following to the database:
Attempted to create employee with salary 19000
Traceback (most recent call last):
  File "path/to/script.py", line 16, in <module>
    bob = Employee('Bob', 19000)
  File "path/to/script.py", line 11, in __init__
    raise SalaryError(salary)
__main__.SalaryError: Salary must be between 20k and 500k, you put 19000
```


We can see here that the `Employee` constructor raised the `SalaryError` exception and printed out the appropriate error message. It also alerts the user that the error is being logged to the database.

This is the benefit of writing custom exceptions. Not only do we help our future debugging efforts by making exceptions searchable, we also can craft descriptive and helpful error messages. Additionally, custom exceptions let us specify the behavior of our program when encountering errors, allowing us to do things like report errors to a database.

Conclusion
----------

In this article, we improved our debugging and quality assurance skills by learning about exceptions and exception handling in Python. We started by learning about handling exceptions, allowing us to define how the occurrence of certain errors affect the behavior of our programs. Then, we learned how to write our own custom exceptions and how doing so can improve our future troubleshooting efforts. These skills come in handy especially as your software system grows in complexity and usage, since this typically leads to interesting program states that need to be specifically handled.

For more Python-specific information on exceptions, check out [chapter 8 in the Python docs](https://docs.python.org/3/tutorial/errors.html).

If you’re trying to get better at Python, try one of my other language-specific tutorials like the delegation/decorator pattern series starting here: [Delegate and Decorate in Python: Part 1 – The Delegation Pattern](https://erikscode.space/index.php/2020/08/01/delegate-and-decorate-in-python-part-1-the-delegation-pattern/ "Delegate and Decorate in Python: Part 1 – The Delegation Pattern").