Exceptions#

Learning Objectives: By the end of this notebook, you should be able to:

  1. Explain how to diagnose the source of an Exception through a Traceback

  2. Use the try and except clause to handle Exceptions in your code

  3. Raise Exceptions and define custom Exceptions

Diagnosing Exceptions#

In Python, “errors” are referred to as “Exceptions”. You can design your code to handle Exceptions that you may expect to occur when your code is written.

The most common exception is the SyntaxError:

# try a print statment without close a set of parentheses
print('There is a problem'
  Cell In[1], line 2
    print('There is a problem'
                              ^
SyntaxError: incomplete input

As you can see, the Exception message will print the type of error that has occurred.

Other common errors include the TypeError:

# try to print a statement that concatenates a string and an integer
print('My favorite number is '+17)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[2], line 2
      1 # try to print a statement that concatenates a string and an integer
----> 2 print('My favorite number is '+17)

TypeError: can only concatenate str (not "int") to str

And the NameError:

# try to print the variable hello
print(hello)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 2
      1 # try to print the variable hello
----> 2 print(hello)

NameError: name 'hello' is not defined

And the AttributeError:

# define a list
my_list = [1,2,3]

# try to add a number to the list using a method called add
my_list.add(4)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 5
      2 my_list = [1,2,3]
      4 # try to add a number to the list using a method called add
----> 5 my_list.add(4)

AttributeError: 'list' object has no attribute 'add'

In each of the above examples, the error report provides a Traceback that allows you to find where in your code your error has occurred. When using a single cell or block of code, the Traceback is usually not very interesting - you can see where the error occurred. However, what if you have functions that rely on other functions?

# define a function called travel time that calculates the time
# it takes to travel a distance d when traveling at a rate r
def travel_time(d, r):
    time = d/r
    return(time)

# define a function called travel_times that will compute the
# travel times given a list of distances ds and a list of
# rates rs. Use the travel_time function above to compute
# each individual time as you loop through ds and rs
def travel_times(ds, rs):
    times = []
    for d, r in zip(ds,rs):
        times.append(travel_time(d,r))
    return(times)

# test 1 - should not generate an Exception
ds_1 = [1,2,3]
rs_1 = [4,5,6]
travel_times(ds_1, rs_1)

# test 2 - should generate an Exception
ds_2 = [1,2,3]
rs_2 = [4,5,0]
travel_times(ds_2, rs_2)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[5], line 25
     23 ds_2 = [1,2,3]
     24 rs_2 = [4,5,0]
---> 25 travel_times(ds_2, rs_2)

Cell In[5], line 14, in travel_times(ds, rs)
     12 times = []
     13 for d, r in zip(ds,rs):
---> 14     times.append(travel_time(d,r))
     15 return(times)

Cell In[5], line 4, in travel_time(d, r)
      3 def travel_time(d, r):
----> 4     time = d/r
      5     return(time)

ZeroDivisionError: division by zero

In the above codeblock, we see lots of details about the error in our code that allows us to find where and why our error occurred. The lowest message on the list described the actual part of the code that generated our error - in this case, we find an error in the d/r statement because one of our r values was set to 0.

However, we also get a lot more information. Reading from the top down, we can traceback the series of functions and commands that led to the Exception. We get information that our error originated with our second call to the travel_times function. Then inside travel_times, we see that we received an error from the travel_time function. And finally, we have an error in the statement d/r - and that error is the ZeroDivisionError, telling us we tried to divide something by 0.

Using this series of tracebacks, Python allows us to work sequentially through a series of functions to determine where our error may have occurred.

🤔 Mini-Exercise#

Goal: Import the matrix_multiplication module and diagnose where the error occurs when running the code block below.

One of the cornerstones of linear algebra is matrix multipliation. Test out the following code and then answer the questions following the code block for the second test.

# import the matrix_multiplication module
import matrix_multiplication as mm

# test 1
A = [[1, 2], [3, 4]]
B = [[1, 0], [0, 1]]
AB = mm.matrix_multiplication_2d(A, B)
print('Test 1 results: AB = ',AB)

# # test 2
A = [[1, 2, 3], [4, 5, 6]]
B = [[1, 0], [0, 1]]
AB = mm.matrix_multiplication_2d(A, B)
print('Test 2 results: AB = ',AB)
Test 1 results: AB =  [[1, 2], [3, 4]]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[6], line 13
     11 A = [[1, 2, 3], [4, 5, 6]]
     12 B = [[1, 0], [0, 1]]
---> 13 AB = mm.matrix_multiplication_2d(A, B)
     14 print('Test 2 results: AB = ',AB)

File ~/Documents/Teaching/Github/intro_to_python_book/python_basics/matrix_multiplication.py:27, in matrix_multiplication_2d(A, B)
     25 # define the result matrix
     26 AB = [[], []]
---> 27 AB[0].append(dot_product(row_A1, col_B1))
     28 AB[0].append(dot_product(row_A1, col_B2))
     29 AB[1].append(dot_product(row_A2, col_B1))

File ~/Documents/Teaching/Github/intro_to_python_book/python_basics/matrix_multiplication.py:5, in dot_product(row, col)
      3 dot_sum = 0
      4 for r in range(len(row)):
----> 5     dot_sum += row[r]*col[r]
      6 return(dot_sum)

IndexError: list index out of range

Questions for test 2:#

  1. What kind of exception occurs?

  2. Where does the exception occur?

  3. What leads to this exception?

💡 Solution#

  1. An IndexError exception occurs.

  2. The exception occurs in the dot_product function.

  3. The dot_product function assumes that the given row and col arguments have the same length. Since the matrix A has more columns that the rows of B (which were passed to the matrix_multiplication_2d function), this leads to an IndexError.

The try and except clause#

In many cases, we might expect that there will be some errors that arise in our code. Suppose, for example, we have a set of 5 files that we would like to access information from. We could write a loop to access all of these files as follows:

# write a loop to read the data from all 5 files in the data directory
for file_number in range(1,6):
    f = open('data/file_'+str(file_number)+'.txt','r')
    line = f.read()
    f.close()
    print(line)
Data from first file
Data from second file
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
Cell In[7], line 4
      2 for file_number in range(1,6):
      3     f = open('data/file_'+str(file_number)+'.txt','r')
----> 4     line = f.read()
      5     f.close()
      6     print(line)

File <frozen codecs>:322, in decode(self, input, final)

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte

Shoot! Looks like there is some issue with our file!

Try and Except#

If you suspect that some of your files might be corrupted, then you could write some code that will skip those files in your processing

# use the try statement to attempt opening each of the files
# use the except statement to provide a message if the opening did not work
for file_number in range(1,6):
    try:
        f = open('data/file_'+str(file_number)+'.txt','r')
        line = f.read()
        f.close()
        print(line)
    except:
        print('Could not open file_'+str(file_number)+'.txt')
Data from first file
Data from second file
Could not open file_3.txt
Data from fourth file
Data from fifth file

As we can see above, the except statement provides a way to catch errors as they arise. The except statement can also be used to check for different errors and write code to address each one. Let’s see an example:

# use the try statement to attempt opening each of the files
# use the except statement to provide a message if the opening did not work
for file_number in range(1,7):
    try:
        f = open('data/file_'+str(file_number)+'.txt','r')
        line = f.read()
        f.close()
        print(line)
    except FileNotFoundError:
        print('Could not open file_'+str(file_number)+'.txt due to a FileNotFoundError')
    except UnicodeDecodeError:
        print('Could not open file_'+str(file_number)+'.txt due to a UnicodeDecodeError')
    except:
        print('Could not open file_'+str(file_number)+'.txt due to some other error')
Data from first file
Data from second file
Could not open file_3.txt due to a UnicodeDecodeError
Data from fourth file
Data from fifth file
Could not open file_6.txt due to a FileNotFoundError

🤔 Mini-Exercise#

Goal: Update the lines from the previous mini-exercise to handle an error from the matrix_multiplication function. If you get a solution with one except clause, try adding another option or two in the case that there are different errors.

💡 Solution#

Hide code cell content
# test 1
try:
    A = [[1, 2], [3, 4]]
    B = [[1, 0], [0, 1]]
    AB = mm.matrix_multiplication_2d(A, B)
    print('Test 1 results: AB = ',AB)
except:
    print('Found an error in test 1')

# test 2
try:
    A = [[1, 2, 3], [4, 5, 6]]
    B = [[1, 0], [0, 1]]
    AB = mm.matrix_multiplication_2d(A, B)
    print('Test 2 results: AB = ',AB)
except IndexError as var:
    print('Found an IndexError - matrices might be the wrong shape')
except:
    print('Found an error in Test 2')
Test 1 results: AB =  [[1, 2], [3, 4]]
Found an IndexError - matrices might be the wrong shape

Raising Exceptions and Defining Custom Exceptions#

As you develop your own modules, you will likely be very aware of the different possible errors that a user might run into. In this case, it is beneficial to raise exceptions in your code or even generate your own types of exceptions that could be caught using except statements in your users’ codes.

Raising Exceptions#

When designing code, you can raise an Exception using the raise keyword

# raise a ValueError Exception
# print a message that the value is not valid
raise ValueError('Your value is not correct')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[11], line 3
      1 # raise a ValueError Exception
      2 # print a message that the value is not valid
----> 3 raise ValueError('Your value is not correct')

ValueError: Your value is not correct

This can be helpful when writing your own function. For example, implement a ValueError in the matrix multiplication module in the scenario that the the columns of B are not equal to the rows of A

# edit the matrix_multiplication_2d to raise a ValueError if the columns of A
# are not equal to the rows of B
# be sure to restart your kernel before running this cell in Jupyter

# import the matrix_multiplication module
import matrix_multiplication_valueerror as mm

# # test 2
A = [[1, 2, 3], [4, 5, 6]]
B = [[1, 0], [0, 1]]
AB = mm.matrix_multiplication_2d(A, B)
print('Test 2 results: AB = ',AB)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[12], line 11
      9 A = [[1, 2, 3], [4, 5, 6]]
     10 B = [[1, 0], [0, 1]]
---> 11 AB = mm.matrix_multiplication_2d(A, B)
     12 print('Test 2 results: AB = ',AB)

File ~/Documents/Teaching/Github/intro_to_python_book/python_basics/matrix_multiplication_valueerror.py:15, in matrix_multiplication_2d(A, B)
      8 def matrix_multiplication_2d(A, B):
      9 
     10     # in matrix multiplication, we define the product
     11     # AB as the dot product of
     12     # each row of A with each column of B
     14     if len(A[0]) != len(B):
---> 15         raise ValueError('The row length of A must equal the column length of B')
     17     # get the rows of A
     18     row_A1 = A[0]

ValueError: The row length of A must equal the column length of B

There are many Exceptions in Python which you can access. For example, check out the following list:

for class_name in Exception.__subclasses__():
    print(class_name.__name__)
Hide code cell output
ArithmeticError
AssertionError
AttributeError
BufferError
EOFError
ImportError
LookupError
MemoryError
NameError
OSError
ReferenceError
RuntimeError
StopAsyncIteration
StopIteration
SyntaxError
SystemError
TypeError
ValueError
Warning
ExceptionGroup
_OptionError
_Error
error
Error
SubprocessError
ArgumentError
error
ZMQBaseError
Error
PickleError
_Stop
TokenError
StopTokenizing
Error
_GiveupOnSendfile
Incomplete
ClassFoundException
EndOfBlock
InvalidStateError
LimitOverrunError
QueueEmpty
QueueFull
TraitError
Empty
Full
error
error
ReturnValueIgnoredError
ArgumentError
ArgumentTypeError
ConfigError
ConfigurableError
ApplicationError
InvalidPortNumber
error
LZMAError
RegistryError
_GiveupOnFastCopy
NoIPAddresses
Error
BadZipFile
LargeZipFile
MessageError
DuplicateKernelError
ErrorDuringImport
NotOneValueFound
KnownIssue
VerifierFailure
CannotEval
OptionError
BdbQuit
Restart
ExceptionPexpect
PtyProcessError
FindCmdError
HomeDirError
ProfileDirError
IPythonCoreError
InputRejected
GetoptError
ErrorToken
PrefilterError
AliasError
Error
Warning
SpaceInInput
DOMException
ValidationError
EditReadOnlyBuffer
_Retry
InvalidLayoutError
HeightIsUnknownError
GuardRejection
ParserSyntaxError
InternalParseError
_PositionUpdatingFinished
SimpleGetItemNotFound
UncaughtAttributeError
HasNoContext
ParamIssue
_JediError
OnErrorLeaf
InvalidPythonEnvironment
HTTPException
InteractivelyDefined
KillEmbedded
Error
ZombieProcessError
KeyReuseError
UnknownKeyError
LeakedCallbackError
BadYieldError
ReturnValueIgnoredError
Return
QueueEmpty
QueueFull
DebuggerInitializationError
ExpatError
Error
ParserSyntaxError
ResolutionError
_Error
UnableToResolveVariableException
InvalidTypeInArgsException

However, you may want also to defined your own exception. We can do this in Python by leveraging the idea that Exceptions are classes - more on this in the classes section - and we can make a subclass as follows:

# define a class exception called CustomException122
class CustomException122(Exception):
    pass

Once you’ve defined your Exception, you can raise it within your code

# raise your custom exception
raise CustomException122('We encountered the error I was expecting')
---------------------------------------------------------------------------
CustomException122                        Traceback (most recent call last)
Cell In[15], line 2
      1 # raise your custom exception
----> 2 raise CustomException122('We encountered the error I was expecting')

CustomException122: We encountered the error I was expecting

🤔 Mini-Exercise#

Goal: Update the matrix_multiplication module to provide a custom error called MatrixSizeError. Use this error in your check that the columns of A must be equal to the rows of B. Then, implement this custome error in the an except block.

# define a new error called MatrixSizeError in the matrix_multiplication module
# then, edit the matrix_multiplication_2d to raise a MatrixSizeError if the columns of A
# are not equal to the rows of B
# be sure to restart your kernel before running this cell

# import the matrix_multiplication module
import matrix_multiplication_sizeerror as mm

# # test 2
A = [[1, 2, 3], [4, 5, 6]]
B = [[1, 0], [0, 1]]

try:
    AB = mm.matrix_multiplication_2d(A, B)
    print('Test 2 results: AB = ',AB)
except mm.MatrixSizeError:
    print('The matrices A and B are the wrong size')
except:
    print('We ran into another error')
The matrices A and B are the wrong size

💡 Solution#

In your matrix_multiplication script, you should have a new class defined as

class MatrixSizeError(Exception):
    pass

and this exception should be called as

raise MatrixSizeError(...)