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:

Python
>>> 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 tryexcept 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:

Python
# 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:

Shell
$ 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:

Python
# 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:

Shell
$ 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:

Python
# 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:

Shell
$ 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:

Python
# 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:

Shell
$ 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:

Shell
$ 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:

Python
# file_fault.py

from os import strerror

try:
    with open("datafile.txt", mode="rt") as f:
        print(f.readlines())
except OSError as error:
    print(strerror(error.errno))

To begin with, you import the os module to allow your code to interact with the operating system. More specifically, you import the os.strerror() method to allow you to see the error message associated with the OSError error code.

Your code will print the contents of a file named datafile.txt, provided it exists. If datafile.txt doesn’t exist, then your code raises a FileNotFoundError. Although your inclusion of only one except clause makes it look like you can only catch an OSError, your handler can also deal with a FileNotFoundError because it’s actually an OSError.

To identify which subclass of OSError you’ve caught, you can use type(error).__name__ to print its class name. However, this is meaningless to most users. Instead, you can identify the underlying error through the .errno attribute. This is a number produced by the operating system that provides information on the problem that raised the OSError. The number itself is meaningless, but its associated error message tells you more about the problem.

You’ve written your handler to use the variable error, which references the OSError subclass raised. To see its associated error message, you pass the error code into the os.strerror() function. When you print the output of the function, you’ll learn exactly what went wrong:

Shell
$ python file_fault.py
No such file or directory

As you can see, the file doesn’t exist. To fully test the code, you need to cause other errors, like a PermissionError. You might like to try this by creating a datafile.txt file, but make sure you don’t have permission to access it. Then try rerunning your code once more and verify that your code recognizes and handles the PermissionError exception.

As an exercise, you might like to try rewriting this program to use type(error).__name__ to identify specific errors by class name. Also, can you think of any other exceptions that this handler is also capable of catching? Perhaps the diagram above will help you figure out the answer.

If you’re interested in learning about the range of specific errors that OSErrror could encounter, then take a look at the error code list below:

If you want a list of the error codes for your platform, you can get one by running this code:

Python
# error_codes.py

import os
import errno
from pprint import pprint

pprint({e: os.strerror(e) for e in sorted(errno.errorcode)})

When you run it, you’ll see the full set of error code values assigned to OSError.errno :

Shell
$ python error_codes.py
{1: 'Operation not permitted',
 2: 'No such file or directory',
 3: 'No such process',
 4: 'Interrupted function call',
 5: 'Input/output error',
 6: 'No such device or address',
 7: 'Arg list too long',
 8: 'Exec format error',
 9: 'Bad file descriptor',
 10: 'No child processes',
 11: 'Resource temporarily unavailable',
 12: 'Not enough space',
 13: 'Permission denied',
 14: 'Bad address',
 16: 'Resource device',
 ...
}

Your code produces a complete list of the error codes from the operating system that ran it, as well as their associated messages. As you can see, these messages provide you with great insight as to what went wrong.

Having spent some time learning how to handle exceptions, you’ll now move on, go against design principle ten of the Zen of Python, and learn how to ignore Exceptions. Madness, you might think! Well, not always, as you’ll now see.

Ignore Multiple Python Exceptions Using contextlib.suppress()

Usually, when your code encounters an exception, you want to handle it. But sometimes, you might need to ignore exceptions to make your code work. Examples may include checking a network card for data you need that hasn’t yet arrived, or reading data from a file that may be currently locked by another user.

The traditional way of ignoring exceptions in Python is to catch them but do nothing with them. You’ll sometimes see code like this:

Python
# exception_pass.py

try:
    with open("file.txt", mode="rt") as f:
        print(f.readlines())
except (FileNotFoundError, PermissionError):
    pass

To ignore the FileNotFoundError that occurs if this file doesn’t exist, you catch the exception in the usual way, then use the pass keyword in the handler to do nothing with it. When you run your code this time, there’s no output, but your program hasn’t crashed either:

Shell
$ python exception_pass.py
$

If you use this technique, your code may confuse your readers. You’re instructing the program to catch an exception, and then you’re telling it not to bother with it. Fortunately, there’s a neater way.

To write code that explicitly suppresses exceptions, Python provides a context manager. To use it, you use the suppress() function from the contextlib module. You decide to rewrite your code using suppress():

Python
# exception_suppress.py

from contextlib import suppress

with suppress(FileNotFoundError, PermissionError):
    with open("file.txt", mode="rt") as f:
        print(f.readlines())

Here, you use suppress() to create a context manager to suppress both FileNotFoundError and PermissionError exceptions, and the with statement applies the context manager to its indented code. Had you raised another exception type, the code would crash. Also, had you written any code outside of with, the suppressions wouldn’t apply.

Why not try running this code against a file that doesn’t exist, then against one to which you don’t have read access? You’ll find that it doesn’t crash.

As another example, suppose that several files, each named transactions.txt, get created at various times during the day. These contain data that you must merge into the all_transactions.txt file. Your code will then archive the original file to transactions.arch_ts, where ts is a timestamp for uniqueness. Sometimes, when your program looks for a file, it won’t be there. You must handle this:

Python
# file_archiver.py

import pathlib
import time
from contextlib import suppress

temporary_file = pathlib.Path("transactions.txt")
main_file = pathlib.Path("all_transactions.txt")

while True:
    archive_path = temporary_file.with_suffix(f".arch_{time.time()}")
    with suppress(FileNotFoundError):
        with temporary_file.open(mode="rt") as transactions:
            with main_file.open(mode="at") as main:
                print("Found new transactions, updating log, & archiving")
                main.writelines(transactions.readlines())
        temporary_file.replace(archive_path)
    time.sleep(3)

You decide to use pathlib to take advantage of its file handling capabilities. First, you create two Path objects that represent the paths to two of the files your program uses. The main body of your code runs in an infinite loop, but you use time.sleep(3) to simulate periodic checking for the presence of a transactions.txt file.

At the start of each iteration of the loop, you use the .with_suffix() method of the Path object to create a new Path with an .arch_ts extension. You then suppress the FileNotFoundError exception. Should this occur within the loop, you ignore it to enable the loop to continue. This is how your code continually checks for the presence of transactions.txt even when its absence raises an exception.

Next, your code processes the file. You open transactions.txt for reading in text mode, and all_transactions.txt for appending to. This is where you might cause the FileNotFoundError exception. If transactions.txt doesn’t exist, then the FileNotFoundError exception will return control of your code back to the with suppress() clause, which will suppress the exception.

If transactions.txt is present, then your code opens or creates the all_transactions.txt file. You then copy the content of transactions.txt to the end of all_transactions.txt before displaying a message to your users. At this point, both context managers end, closing both files automatically. Your code then renames transactions.txt with its new timestamped name.

Regardless of whether or not transactions.txt exists, your code will then sleep for three seconds before the start of the next iteration of the loop before rechecking to see if the file is now present.

To try this out, download the code and three sample files named transactions1.txt, transactions2.txt, and transactions3.txt into the same folder. You can get them at the link below:

After you start the program, rename transactions1.txt to transactions.txt. Your program will soon find it and merge its content into all_transactions.txt file before renaming it. Do the same for the other transaction files. To close the program, use Ctrl+C or the stop button on your Python IDLE.

You’ll know everything has worked when you see output similar to the following:

Shell
$ python file_archiver.py
Found new transactions, updating log, & archiving
Found new transactions, updating log, & archiving
Found new transactions, updating log, & archiving
...

Once it completes, your program will have created three archived files with unique names. In addition, your all_transactions.txt file should now contain the content of the three archived files.

You may also notice that an unhandled KeyBoardInterrupt exception has caused the program to crash. This happens if you use the Ctrl+C keys to terminate the code. Because you didn’t catch the KeyBoardInterrupt exception, your code had no option other than to crash out. Perhaps you could try handling this exception as well.

Up to this point, you’ve learned several ways of dealing with multiple possible exceptions. However, you’ve only ever caught one out of the bunch. In the final section, you’ll learn how to catch each member in a group of exceptions.

Catch Multiple Python Exceptions Using Exception Groups

When you use tryexcept in your code, it’s actually only capable of catching the first exception that occurs within the try block. If you try to raise multiple exceptions, the program will finish after handling the first exception. The rest will never be raised. You can show this using code similar to the following:

Python
# catch_first_only.py

exceptions = [ZeroDivisionError(), FileNotFoundError(), NameError()]
num_zd_errors = num_fnf_errors = num_name_errors = 0

try:
    for e in exceptions:
        raise e
except ZeroDivisionError:
    num_zd_errors += 1
except FileNotFoundError:
    num_fnf_errors += 1
except NameError:
    num_name_errors += 1
finally:
    print(f"ZeroDivisionError was raised {num_zd_errors} times.")
    print(f"FileNotFoundError was raised {num_fnf_errors} times.")
    print(f"NameError was raised {num_name_errors} times.")

Your program starts by defining a list of three Exception objects. You then initialize three counter variables to zero. You write a try statement that loops through your list and attempts to raise each exception in turn. Next, you run the code to find out exactly what gets handled:

Shell
$ python catch_first_only.py
ZeroDivisionError was raised 1 times.
FileNotFoundError was raised 0 times.
NameError was raised 0 times.

As you can see from the output, your code falls short of the requirements. While the ZeroDivisionError exception gets raised and handled, none of your other handlers have any effect. Only the num_zd_errors variable gets incremented.

You also used a finally clause in your code. This clause runs regardless of whether or not any exceptions have occurred. You’ve used it here to print the count of each exception raised.

Sometimes, you may need to handle all the exceptions that occur. For example, this is necessary in concurrent programming, where your program performs multiple tasks simultaneously. In cases where you have concurrent tasks running, each task can raise its own exceptions. Beginning with Python 3.11, the language supports ExceptionGroup objects and a special except* clause to allow you to handle all exceptions.

To investigate exception groups, you decide to adapt your earlier code to learn how you can deal with multiple exceptions. For this, you use the special except* syntax:

Python
# catch_all.py

exceptions = [ZeroDivisionError(), FileNotFoundError(), NameError()]
num_zd_errors = num_fnf_errors = num_name_errors = 0

try:
    raise ExceptionGroup("Errors Occurred", exceptions)
except* ZeroDivisionError:
    num_zd_errors += 1
except* FileNotFoundError:
    num_fnf_errors += 1
except* NameError:
    num_name_errors += 1
finally:
    print(f"ZeroDivisionError was raised {num_zd_errors} times.")
    print(f"FileNotFoundError was raised {num_fnf_errors} times.")
    print(f"NameError was raised {num_name_errors} times.")

In this version of your code, your try clause raises a new ExceptionGroup object that’s instantiated to contain the content of your exceptions list. When your program raises this, it passes all of the exceptions to the handlers.

To accommodate your group and make sure that all handlers can process all exceptions, as opposed to just the first one, the handlers use except* instead of the plain except. This means that when the ExceptionGroup is raised, the except* ZeroDivisionError clause handles any ZeroDivisionError exceptions. Your program then passes the outstanding exceptions to the except* FileNotFoundError clause and so on, down through each handler.

Next you run it to find out exactly what gets handled, hoping it’s more than the last time:

Shell
$ python python catch_all.py
ZeroDivisionError was raised 1 times.
FileNotFoundError was raised 1 times.
NameError was raised 1 times.

Success! As you can see from the output, your program successfully handled all of your exceptions and correctly incremented all three variables. To investigate further, why not adjust your list to contain multiple instances of each exception and see what happens? Then try adding some exceptions to the list that you haven’t included in the handler. What happens now?

Conclusion

You now know how to use some of the more subtle features of Python’s exception handling mechanism. You can handle exceptions in more powerful ways than you may have thought possible. You also understand that sometimes ignoring exceptions is necessary to make sure your code does what you want it to.

After reading this tutorial, you can now:

  • Catch one of several possible exceptions
  • Identify the exception that you’ve caught
  • Catch exceptions using their superclass
  • Ignore exceptions and understand why you’d do this
  • Catch multiple exceptions and handle them all

You’re well on your way to becoming an exceptional exception handler! If you have any questions, feel free to reach out using the comments section below.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Ian Eyre

Ian Eyre Ian Eyre

Ian is an avid Pythonista and Real Python contributor who loves to learn and teach others.

» More about Ian

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Tutorial Categories: best-practices intermediate python