June 7, 2022

Python Exceptions: The Ultimate Beginner’s Guide (with Examples)

This tutorial covers exceptions in Python, including why they occur, how to identify them, and how to resolve them.

In this tutorial, we will define exceptions in Python, we will identify why they are important and how they differ from syntax errors, and we will learn how to resolve exceptions in Python.

What Are Exceptions in Python?

When an unexpected condition is encountered while running a Python code, the program stops its execution and throws an error. There are basically two types of errors in Python: syntax errors and exceptions. To understand the difference between these two types, let's run the following piece of code:

print(x
print(1)
  File "C:\Users\Utente\AppData\Local\Temp/ipykernel_4732/4217672763.py", line 2
    print(1)
    ^
SyntaxError: invalid syntax

A syntax error was thrown since we forgot to close the parenthesis. This type of error is always raised when we use a statement that is syntactically incorrect in Python. The parser shows the place where the syntax error was detected by a little arrow ^. Notice also that the subsequent line print(1) was not executed since the Python interpreter stopped working when the error occurred.

Let's fix this error and re-run the code:

print(x)
print(1)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/971139432.py in 
----> 1 print(x)
      2 print(1)

NameError: name 'x' is not defined

Now when we have fixed the wrong syntax, we got another type of error: an exception. In other words, an exception is a type of error that occurs when a syntactically correct Python code raises an error. An arrow indicates the line where the exception occurred, while the last line of the error message specifies the exact type of exception and provides its description to facilitate debugging. In our case, it is a NameError since we tried to print the value of a variable x that was not defined before. Also in this case, the second line of our piece of code print(1) was not executed because the normal flow of the Python program was interrupted.

To prevent a sudden program crash, it is important to catch and handle exceptions. For example, provide an alternative version of the code execution when the given exception occurs. This is what we are going to learn next.

Standard Built-in Types of Exceptions

Python provides many types of exceptions thrown in various situations. Let's take a look at the most common built-in exceptions with their examples:

  • NameError – Raised when a name doesn't exist among either local or global variables:
print(x)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/1353120783.py in 
----> 1 print(x)

NameError: name 'x' is not defined
  • TypeError – Raised when an operation is run on an inapplicable data type:
print(1+'1')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/218464413.py in 
----> 1 print(1+'1')

TypeError: unsupported operand type(s) for +: 'int' and 'str'
  • ValueError – Raised when an operation or function takes in an invalid value of an argument:
print(int('a'))
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/3219923713.py in 
----> 1 print(int('a'))

ValueError: invalid literal for int() with base 10: 'a'
  • IndexError – Raised when an index doesn't exist in an iterable:
print('dog'[3])
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/2163152480.py in 
----> 1 print('dog'[3])

IndexError: string index out of range
  • IndentationError – Raised when indentation is incorrect:
for i in range(3):
print(i)
  File "C:\Users\Utente\AppData\Local\Temp/ipykernel_4732/3296739069.py", line 2
    print(i)
    ^
IndentationError: expected an indented block
  • ZeroDivisionError – Raised at attempt to divide a number by zero:
print(1/0)
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/165659023.py in 
----> 1 print(1/0)

ZeroDivisionError: division by zero
  • ImportError – Raised when an import statement is incorrect:
from numpy import pandas
---------------------------------------------------------------------------

ImportError                               Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/3955928270.py in 
----> 1 from numpy import pandas

ImportError: cannot import name 'pandas' from 'numpy' (C:\Users\Utente\anaconda3\lib\site-packages\numpy\__init__.py)
  • AttributeError – Raised at attempt to assign or refer an attribute inapplicable for a given Python object:
print('a'.sum())
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/2316121794.py in 
----> 1 print('a'.sum())

AttributeError: 'str' object has no attribute 'sum'
  • KeyError – Raised when the key is absent in the dictionary:
animals = {'koala': 1, 'panda': 2}
print(animals['rabbit'])
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/1340951462.py in 
      1 animals = {'koala': 1, 'panda': 2}
----> 2 print(animals['rabbit'])

KeyError: 'rabbit'

For a full list of Python built-in exceptions, please consult the Python Documentation.

Handling Exceptions in Python

Since raising an exception results in an interruption of the program execution, we have to handle this exception in advance to avoid such undesirable cases.

The try and except Statements

The most basic commands used for detecting and handling exceptions in Python are try and except.

The try statement is used to run an error-prone piece of code and must always be followed by the except statement. If no exception is raised as a result of the try block execution, the except block is skipped and the program just runs as expected. In the opposite case, if an exception is thrown, the execution of the try block is immediately stopped, and the program handles the raised exception by running the alternative code determined in the except block. After that, the Python script continues working and executes the rest of the code.

Let's see how it works by the example of our initial small piece of code print(x), which raised earlier a NameError:

try:
    print(x)
except:
    print('Please declare the variable x first')

print(1)
Please declare the variable x first
1

Now that we handled the exception in the except block, we received a meaningful customized message of what exactly went wrong and how to fix it. What's more, this time, the program didn't stop working as soon as it encountered the exception and executed the rest of the code.

In the above case, we anticipated and handled only one type of exception, more specifically, a NameError. The drawback of this approach is that the piece of code in the except clause will treat all types of exceptions in the same way, and output the same message Please declare the variable x first. To avoid this confusion, we can explicitly mention the type of exception that we need to catch and handle right after the except command:

try:
    print(x)
except NameError:
    print('Please declare the variable x first')
Please declare the variable x first

Handling Multiple Exceptions

Clearly stating the exception type to be caught is needed not only for the sake of code readability. What's more important, using this approach, we can anticipate various specific exceptions and handle them accordingly.

To understand this concept, let's take a look at a simple function that sums up the values of an input dictionary:

def print_dict_sum(dct):
    print(sum(dct.values()))

my_dict = {'a': 1, 'b': 2, 'c': 3}
print_dict_sum(my_dict)
6

Trying to run this function, we can have different issues if we accidentally pass to it a wrong input. For example, we can make an error in the dictionary name resulting in an inexistent variable:

print_dict_sum(mydict)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/2473187932.py in 
----> 1 print_dict_sum(mydict)

NameError: name 'mydict' is not defined

Some of the values of the input dictionary can be a string rather than numeric:

my_dict = {'a': '1', 'b': 2, 'c': 3}
print_dict_sum(my_dict)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/2621846538.py in 
      1 my_dict = {'a': '1', 'b': 2, 'c': 3}
----> 2 print_dict_sum(my_dict)

~\AppData\Local\Temp/ipykernel_4732/3241128871.py in print_dict_sum(dct)
      1 def print_dict_sum(dct):
----> 2     print(sum(dct.values()))
      3 
      4 my_dict = {'a': 1, 'b': 2, 'c': 3}
      5 print_dict_sum(my_dict)

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Another option allows us to pass in an argument of an inappropriate data type for this function:

my_dict = 'a'
print_dict_sum(my_dict)
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/1925769844.py in 
      1 my_dict = 'a'
----> 2 print_dict_sum(my_dict)

~\AppData\Local\Temp/ipykernel_4732/3241128871.py in print_dict_sum(dct)
      1 def print_dict_sum(dct):
----> 2     print(sum(dct.values()))
      3 
      4 my_dict = {'a': 1, 'b': 2, 'c': 3}
      5 print_dict_sum(my_dict)

AttributeError: 'str' object has no attribute 'values'

As a result, we have at least three different types of exceptions that should be handled differently: NameError, TypeError, and AttributeError. For this purpose, we can add multiple except blocks (one for each exception type, three in our case) after a single try block:

try: 
    print_dict_sum(mydict)
except NameError:
    print('Please check the spelling of the dictionary name')
except TypeError:
    print('It seems that some of the dictionary values are not numeric')
except AttributeError:
    print('You should provide a Python dictionary with numeric values')
Please check the spelling of the dictionary name

In the code above, we provided a nonexistent variable name as an input to the function inside the try clause. The code was supposed to throw a NameError but it was handled in one of the subsequent except clauses, and the corresponding message was output.

We can also handle the exceptions right inside the function definition. Important: We can't handle a NameError exception for any of the function arguments since in this case, the exception happens before the function body starts. For example, in the code below:

def print_dict_sum(dct):
    try:
        print(sum(dct.values()))
    except NameError:
        print('Please check the spelling of the dictionary name')
    except TypeError:
        print('It seems that some of the dictionary values are not numeric')
    except AttributeError:
        print('You should provide a Python dictionary with numeric values')

print_dict_sum({'a': '1', 'b': 2, 'c': 3})
print_dict_sum('a')
print_dict_sum(mydict)
It seems that some of the dictionary values are not numeric
You should provide a Python dictionary with numeric values

---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/3201242278.py in 
     11 print_dict_sum({'a': '1', 'b': 2, 'c': 3})
     12 print_dict_sum('a')
---> 13 print_dict_sum(mydict)

NameError: name 'mydict' is not defined

The TypeError and AttributeError were successfully handled inside the function and the corresponding messages were output. Instead, because of the above-mentioned reason, the NameError was not handled properly despite introducing a separate except clause for it. Therefore, the NameError for any argument of a function can't be handled inside the function body.

It is possible to combine several exceptions as a tuple in one except clause if they should be handled in the same way:

def print_dict_sum(dct):
    try:
        print(sum(dct.values()))
    except (TypeError, AttributeError):
        print('You should provide a Python DICTIONARY with NUMERIC values')

print_dict_sum({'a': '1', 'b': 2, 'c': 3})
print_dict_sum('a')
You should provide a Python DICTIONARY with NUMERIC values
You should provide a Python DICTIONARY with NUMERIC values

The else Statement

In addition to the try and except clauses, we can use an optional else command. If present, the else command must be placed after all the except clauses and executed only if no exceptions occurred in the try clause.

For example, in the code below, we tried the division by zero:

try:
    print(3/0)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')
You cannot divide by zero

The exception was caught and handled in the except block and hence the else clause was skipped. Let's take a look at what happens if we provide a non-zero number:

try:
    print(3/2)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')
1.5
The division is successfully performed

Since no exception was raised, the else block was executed and output the corresponding message.

The finally Statement

Another optional statement is finally, if provided, it must be placed after all the clauses including else (if present)
and executed in any case, whether or not an exception was raised in the try clause.

Let's add the finally block to both of the previous pieces of code and observe the results:

try:
    print(3/0)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')
finally:
    print('This message is always printed')
You cannot divide by zero
This message is always printed
try:
    print(3/2)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')
finally:
    print('This message is always printed')
1.5
The division is successfully performed
This message is always printed

In the first case, an exception was raised, in the second there was not. However, in both cases, the finally clause outputs the same message.

Raising an Exception

Sometimes, we may need to deliberately raise an exception and stop the program if a certain condition occurs. For this purpose, we need the raise keyword and the following syntax:

raise ExceptionClass(exception_value)

Above, ExceptionClass is the type of exception to be raised (e.g., TypeError) and exception_value is an optional customized descriptive message that will be displayed if the exception is raised.

Let's see how it works:

x = 'blue'
if x not in ['red', 'yellow', 'green']:
    raise ValueError
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/2843707178.py in 
      1 x = 'blue'
      2 if x not in ['red', 'yellow', 'green']:
----> 3     raise ValueError

ValueError: 

In the piece of code above, we didn't provide any argument to the exception and therefore the code didn't output any message (by default, the exception value is None).

x = 'blue'
if x not in ['red', 'yellow', 'green']:
    raise ValueError('The traffic light is broken')
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4732/359420707.py in 
      1 x = 'blue'
      2 if x not in ['red', 'yellow', 'green']:
----> 3     raise ValueError('The traffic light is broken')

ValueError: The traffic light is broken

We ran the same piece of code, but this time we provided the exception argument. In this case, we can see an output message that gives more context to why exactly this exception occurred.

Conclusion

In this tutorial, we discussed many aspects regarding exceptions in Python. In particular, we learned the following:

  • How to define exceptions in Python and how they differ from syntax errors
  • What built-in exceptions exist in Python and when they are raised
  • Why it is important to catch and handle exceptions
  • How to handle one or multiple exceptions in Python
  • How different clauses for catching and handling exceptions work together
  • Why it's important to specify the type of exception to be handled
  • Why we can't handle a NameError for any argument of a function inside the function definition
  • How to raise an exception
  • How to add a descriptive message to a raised exception and why it is a good practice

With these skills, you're ready to cope with any real-world data science tasks that require resolving exceptions in Python.

Elena Kosourova

About the author

Elena Kosourova

Elena is a petroleum geologist and community manager at Dataquest. You can find her chatting online with data enthusiasts and writing tutorials on data science topics. Find her on LinkedIn.