How to Catch Multiple Exceptions in Python

How to Catch Multiple Exceptions in Python

by Ian Eyre Sep 20, 2023 best-practices intermediate python

In this tutorial, you’ll learn various techniques to catch multiple exceptions with Python. To begin with, you’ll review Python’s exception handling mechanism before diving deeper and learning how to identify what you’ve caught, sometimes ignore what you’ve caught, and even catch lots of exceptions.

Python raises an exception when your code encounters an occasional but not unexpected error. For example, this will occur if you try to read a missing file. Because you’re aware that such exceptions may occur, you should write code to deal with, or handle, them. In contrast, a bug happens when your code does something illogical, like a miscalculation. Bugs should be fixed, not handled. This is why debugging your code is important.

When your Python program encounters an error and raises an exception, your code will probably crash, but not before providing a message within a traceback indicating what the problem is:

>>>
>>> 12 / "five"
Traceback (most recent call last):
  ...
TypeError: unsupported operand type(s) for /: 'int' and 'str'

Here, you’ve tried to divide a number by a string. Python can’t do this, so it raises a TypeError exception. It then shows a traceback reminding you that the division operator doesn’t work with strings.

To allow you to take action when an error occurs, you implement exception handling by writing code to catch and deal with exceptions. Better this than your code crashing and scaring your user. To handle exceptions, you use the try statement. This allows you to monitor code for exceptions and take action should they occur.

Most try statements use try … except blocks as follows:

  • The try block contains the code that you wish to monitor for exceptions. Any exceptions raised within try will be eligible for handling.

  • One or more except blocks then follow try. These are where you define the code that will run when exceptions occur. In your code, any raised exceptions trigger the associated except clause. Note that where you have multiple except clauses, your program will run only the first one that triggers and then ignore the rest.

To learn how this works, you write a try block to monitor three lines of code. You include two except blocks, one each for ValueError and ZeroDivisionError exceptions, to handle them should they occur:

# handler_statement.py

try:
    first = float(input("What is your first number? "))
    second = float(input("What is your second number? "))
    print(f"{first} divided by {second} is {first / second}")
except ValueError:
    print("You must enter a number")
except ZeroDivisionError:
    print("You can't divide by zero")

The code that you’re monitoring asks the user to enter both numbers and then prints the division. You’ll cause a ValueError if you don’t enter a number in the first two lines of code. When the float() function tries to convert your input into a float, a ValueError occurs if this isn’t possible. A ZeroDivisionError occurs if you enter 0 as the second number. When the print() function attempts to divide by zero, you get a ZeroDivisionError.

Having written the above code, you then test each of the control flows. To do this, you first provide perfectly valid data, then provide a string for the second number, and finally provide a 0 for the second number:

$ python handler_statement.py
What is your first number? 10
What is your second number? 5
10.0 divided by 5.0 is 2.0

$ python handler_statement.py
What is your first number? 10
What is your second number? "five"
You must enter a number

$ python handler_statement.py
What is your first number? 10
What is your second number? 0
You can't divide by zero

The good news is that your code never crashes. This is because your code has successfully handled the exceptions.

First, you provided acceptable data. When you look at the output, you can see that the program flows only through try. You haven’t invoked any of the except clauses because Python hasn’t raised any exceptions.

You then cause a ValueError by entering a string. This happens because the float() function can’t convert your "five" into a float. Your program flow now becomes try then except ValueError. Despite raising a ValueError, your code has handled it gracefully. Your users will no longer experience a worrying crash.

In your final test run, you try to divide by 0. This time, you cause a ZeroDivisionError because Python doesn’t like your enthusiasm for Riemann spheres and infinity. This time, the program flow is try then except ZeroDivisionError. Again, your code has handled your exception gracefully. Most of your users will be happy with this, though the mathematicians may be disappointed.

After the error handling is complete, your program’s flow would usually continue with any code beyond the try statement. In this case, there’s none, so the program simply ends.

As an exercise, you might like to try entering a string as the first input and a number as the second. Can you predict what will happen before you try it out?

Up to this point, you’ve reviewed how to catch exceptions individually using the try statement. In the remainder of this tutorial, you’ll learn about ways that you can catch multiple exceptions. Time to dive a bit deeper.

How to Catch One of Several Possible Python Exceptions

Catching individual exceptions in separate except clauses is the way to go if you need to perform different handling actions on the different exceptions being caught. If you find that you’re performing the same actions in response to different exceptions, then you can produce simpler, more readable code by handling multiple exceptions in a single except clause. To do this, you specify the exceptions as a tuple within the except statement.

Suppose you now like the idea of your earlier code being able to handle both exceptions in a single line. To do this, you decide to rewrite your code as follows:

# multiple_exceptions.py

try:
    first = float(input("What is your first number? "))
    second = float(input("What is your second number? "))
    print(f"{first} divided by {second} is {first / second}")
except (ValueError, ZeroDivisionError):
    print("There was an error")

This time, if you catch either a ValueError or ZeroDivisionError exception, you handle it with the same except clause. You also could’ve included additional except clauses for other exceptions if you wanted to. These would’ve worked for you in the same way as before.

You can then test this version of your code using the same test cases as before:

$ python multiple_exceptions.py
What is your first number? 10
What is your second number? 5
10.0 divided by 5.0 is 2.0

$ python multiple_exceptions.py
What is your first number? 10
What is your second number? "five"
There was an error

$ python multiple_exceptions.py
What is your first number? 10
What is your second number? 0
There was an error

In your first test, only your try block gets executed because you don’t raise any exceptions. Your second and third test cases raise a ValueError and ZeroDivisionError, respectively. You’ve caught each of them in the same except clause. In both cases, the program flow is try then except (ValueError, ZeroDivisionError). Again, your code survives the exceptions without crashing.

Although you’re happy that the handler safely handles both exceptions in the same way, you’d like to know exactly which exception is being raised. You’ll learn how to do this next.

Identify Which Python Exception Was Caught

If you’re familiar with object-oriented programming concepts, then you know that classes are templates that define the content and abilities of the objects that you create, or instantiate, from them. When your code raises a Python exception, it actually instantiates an object from the class that defines the exception. For example, when you raise a ValueError exception, you’re instantiating an instance of the ValueError class.

While you don’t need a thorough knowledge of object-oriented programming to work with exceptions, you should be aware that different exception objects only exist because they’re instantiated from different classes.

You decide to try and identify the individual exceptions caught in your previous code:

# exception_identification.py

try:
    first = float(input("What is your first number? "))
    second = float(input("What is your second number? "))
    print(f"{first} divided by {second} is {first / second}")
except (ValueError, ZeroDivisionError) as error:
    print(f"A {type(error).__name__} has occurred.")

Here, you’ve made some adjustments to the handler. It still catches both ValueError and ZeroDivisionError exceptions, but this time, you assign the exception object to a variable named error. Doing this allows you to analyze it.

To find the class of an Exception, you use type(). If you print the .__name__ attribute of the class, then you see exactly which exception your code has handled. Now you can find out which exceptions have occurred:

$ python exception_identification.py
What is your first number? 10
What is your second number? 5
10.0 divided by 4.0 is 2.0

$ python exception_identification.py
What is your first number? 10
What is your second number? "five"
A ValueError has occurred.

$ python exception_identification.py
What is your first number? 10
What is your second number? 0
A ZeroDivisionError has occurred.

The first test is a relief because it proves that your update still works. The second and third tests raise exceptions. As you can see, your code has correctly identified both the ValueError and ZeroDivisionError exceptions.

Suppose you decide to improve your code by allowing multiplication. You also decide to manually raise a RuntimeError if your user requests an unsupported operation or does something unexpected, like entering a carriage return:

# exception_identification_with_structural_pattern_matching.py

from operator import mul, truediv

def calculate(operator, operand1, operand2):
    return operator(operand1, operand2)

try:
    first = float(input("What is your first number? "))
    second = float(input("What is your second number? "))
    operation = input("Enter either * or /: ")
    if operation == "*":
        answer = calculate(mul, first, second)
    elif operation == "/":
        answer = calculate(truediv, first, second)
    else:
        raise RuntimeError(f"'{operation}' is an unsupported operation")
except (RuntimeError, ValueError, ZeroDivisionError) as error:
    print(f"A {type(error).__name__} has occurred")
    match error:
        case RuntimeError():
            print(f"You have entered an invalid symbol: {error}")
        case ValueError():
            print(f"You have not entered a number: {error}")
        case ZeroDivisionError():
            print(f"You can't divide by zero: {error}")
else:
    print(f"{first} {operation} {second} = {answer}")

This time, you use the operator module’s mul() and truediv() functions to perform multiplication and division. You pass these to your calculate() function, along with two numbers. Your calculate() function then calls the operator module function that you’ve passed to it, which performs the calculation. This only works if you enter in two numbers and either / or * for the operation.

Although you could’ve coded calculate() to use the * or / operator directly, using an operator module function simplifies the function’s code and provides extensibility.

If your user enters an invalid operator, then your code explicitly raises a RuntimeError. Like any other exception, this will cause the code to crash if you don’t handle it.

The except clause of your handler again contains a tuple of exceptions that you want to catch. You reference the exception, as before, by a variable named error. First, your handler prints the name of the exception class, regardless of the exception raised. Then, you use the match block to print a message based on the specific exception being handled.

You opt for structural pattern matching instead of many elif clauses for neatness. The exception that your error variable references is compared to the various case clauses. Only the first matching case block executes, and the rest are ignored. The chosen case block then prints a message followed by error. The inclusion of error in your f-string prints the exception’s default traceback message for more context.

If the try clause raises no more exceptions, then your program ignores the except clause, and control passes to the else clause. You use it here to print the answer of the calculation.

Now you retest calculate() with normal inputs:

$ python exception_identification_with_structural_pattern_matching.py
What is your first number? 10
What is your second number? 4
Enter either * or /: /
10.0 / 4.0 = 2.5

$ python exception_identification_with_structural_pattern_matching.py
What is your first number? 10
What is your second number? 5
Enter either * or /: *
10.0 * 5.0 = 50.0

As you see, the division still works, and so does the new multiplication capability. Expand the section below to see what happens when you create exceptions.

In these tests, your exceptions don’t crash the code:

$ python exception_identification_with_structural_pattern_matching.py
What is your first number? 10
What is your second number? 5
Enter either * or /: +
A RuntimeError has occurred
You have entered an invalid symbol: '+' is an unsupported operation

$ python exception_identification_with_structural_pattern_matching.py
What is your first number? 10
What is your second number? 5
Enter either * or /:
A RuntimeError has occurred
You have entered an invalid symbol: '' is an unsupported operation

$ python exception_identification_with_structural_pattern_matching.py
What is your first number? 10
What is your second number? "five"
A ValueError has occurred
You have not entered a number: could not convert string to float: '"five"'

$ python exception_identification_with_structural_pattern_matching.py
What is your first number? 10
What is your second number? 0
Enter either * or /: /
A ZeroDivisionError has occurred
You can't divide by zero: float division by zero

Take a look at the last two lines of each test. Your exception handlers have successfully done their job.

This time, you perform a wider range of tests. First of all, you attempt an addition, and then you fail to specify an operation. Both result in a RuntimeError. Your final tests are identical to those that you performed previously. As you can see, your code doesn’t crash, and it correctly identifies the exception raised.

Why not test this code more fully to consolidate your understanding? You may also like to delve deeper into the operator module and use it to expand your previous example with support for addition, subtraction, or any other operations that you wish.

Now that you know how to identify different Exception objects using their classes, next you’ll learn how to use Python’s exception hierarchy to simplify the handling of exceptions by handling their parent exception instead.

Catch One of Multiple Possible Python Exceptions Using Its Superclass

You’ve already learned how different exceptions are objects instantiated from different classes. These classes all belong to the Python exception class hierarchy. All Python exceptions inherit from a class named BaseException, and one of these subclasses is the Exception class. This is the superclass of all of the exceptions that you’ll learn about in this tutorial.

Python contains over sixty different exceptions. The diagram below illustrates only a few, but it does contain all the Exception subclasses that you’ll cover in this tutorial. Indeed, these are some of the common exceptions that you’re likely to encounter:

The main exception classes in Python.

The chart shows that Exception is the superclass of all other exceptions. Subclasses of Exception inherit everything that Exception contains. Usually, subclasses extend their inherited members to differentiate themselves. In the case of exceptions, inheritance is mostly about creating an exception hierarchy. As you can see, ArithmeticError is a subclass of Exception. Internally, the differences between them are negligible.

You can also see two of the subclasses of the OSError class, namely PermissionError and FileNotFoundError. Again, this means that both PermissionError and FileNotFoundError are actually subclasses of OSError. By implication, both are also Exception subclasses because OSError inherits from Exception.

You can use the fact that a subclass is a variant of its superclass to your advantage to capture different exceptions. Suppose you write the following:

# file_fault.py

from os import strerror

try:
    with open("datafile.txt", mode="rt") as f:
        print(f.readlines())