Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

9 Understanding Errors

9.1Lesson goals

9.2Overview

When writing code in any language, the first draft will hardly ever be perfect. Even the second, third, or even the fifth draft of our code can have errors, which are known as bugs in programming. These bugs can range from simple syntax errors to complicated logic problems. Fortunately, Python has built-in tools in diagnosing, logging, and reporting programming bugs. Therefore, to become proficient at writing Python code, we need to become comfortable in understanding Python error logs. In this lesson, we will look at how to read a Python traceback, how to work around errors with the try-except commands, and how to raise errors when needed contextual information is needed to correct a bug. We will also look at a general troubleshooting approach by searching through library documentation.

9.3Error messages

When Python runs into an error it will output a logging report called a traceback that shows the line where an exception has occurred, which has caused the Python program to quit. The traceback will show what part of the code started the exception process and then “traces back” step-by-step through the code blocks to ultimately where the error originated. Following this step by step logging, the traceback will show the exception type and provide an error message that will hint towards what is wrong with the code.

Let’s demonstrate how a traceback works using the following example:

def fail():
   asdf

def start():
   fail()

start()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 7
      3 
      4 def start():
      5    fail()
      6 
----> 7 start()

Cell In[1], line 5, in start()
      4 def start():
----> 5    fail()

Cell In[1], line 2, in fail()
      1 def fail():
----> 2    asdf

NameError: name 'asdf' is not defined

In this example, we can see the traceback begins with an arrow pointed at the start() function command. This is the call that started the chain of events that led to the exception:

Cell In[1], line 7
      3 
      4 def start():
      5    fail()
      6 
----> 7 start()

There is nothing wrong with this call by itself. The start() function is able to successfully execute its code block and call the next function fail() that is in our traceback. Note the first line of this section, In[1], line 5, in start()), shows the calling of fail() happened in the start() code block:

Cell In[1], line 5, in start()
      4 def start():
----> 5    fail()

The last call of the traceback shows the line of code that was the root cause of our exception:

Cell In[1], line 2, in fail()
      1 def fail():
----> 2    asdf

The traceback then prints out the exception type and the error message:

NameError: name 'asdf' is not defined

Here, the exception type (NameError) is not as important as the error message: name 'asdf' is not defined. The traceback has reported that asdf is defined before it is referenced in the code. Therefore, the error in our code is that a value is not assigned to asdf. From this error message we can deduce a NameError error type refers to when a variable name is called without having a value assigned to it.

Following a traceback is important to debug your code. When running this code cell as a file, we can see that the traceback still shows the relevant line numbers in the Python file:

Traceback (most recent call last):
 File "./fail.py", line 7, in <module>
   start()
 File "./fail.py", line 5, in start
   fail()
 File "./fail.py", line 2, in fail
   asdf
NameError: name 'asdf' is not defined

Let’s register another type of error called a ValueError. This error type often occurs when we pass an argument that is of the right type (e.g., program expects a float and receives a float) but has an incorrect value. A good example of this is when calculating the square root of a negative number. In this example, create a simple program that takes the square root of the following list of numbers:

2 2 , 4 4 , 16 16 , 5 -5 , 253 253 , 2.5 -2.5 , and 100 100

Enter this set of number as a list object, and pass this list through a for loop that will take the square root of each number. Have the for loop print out a phrase that mentions both the number and its square root.

The built-in Python library does not have a square root function. However, the math standard library has this capability with the sqrt() function. Therefore, you will need to import this library into your code. You can review how to import the math library here.


Solution:

As seen in the problem statement, 2.5 -2.5 and 5 -5 should give us an error. The code below demonstrates the ValueError exception using the given code requirements:

from math import sqrt

list = [2, 4, 16, -5, 253, -2.5, 100]

for i in list:
    print(f"The square root of {i} is {sqrt(i):.5f}.")
The square root of 2 is 1.41421.
The square root of 4 is 2.00000.
The square root of 16 is 4.00000.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 6
      2 
      3 list = [2, 4, 16, -5, 253, -2.5, 100]
      4 
      5 for i in list:
----> 6     print(f"The square root of {i} is {sqrt(i):.5f}.")

ValueError: math domain error

As seen above, the for loop successfully iterates over three loops before raising the ValueError exception on -5. The example in the next section will show how we can have the program flag this error, while also continuing through the remaining values.


9.4Error catching with try and except

When writing code, we may be dealing with input data that are not perfectly uniform. For instance, we may be parsing a CSV file that supposedly only contains integers but a string is found in the file. This may cause our Python program to crash when it reaches the string. In order to provide some “flexibility” in how our program operates, we would prefer that our Python code could try an alternative block of code to resolve the issue rather than simply crashing. Fortunately, the try - except coding pattern allows us to implement alternative code blocks to handle these exceptions.

To see how this all works, let us start with the following code block, which will initially fail:

def fail(my_list):
   """Tallies up total from a list."""
   a = 0
   for i in my_list:
       a += i
   return a

# tally up total from provided list
b = fail([1, 2, "3", 4])
# print total
print("b:", b)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 9
      5        a += i
      6    return a
      7 
      8 # tally up total from provided list
----> 9 b = fail([1, 2, "3", 4])
     10 # print total
     11 print("b:", b)

Cell In[3], line 5, in fail(my_list)
      1 def fail(my_list):
      2    """Tallies up total from a list."""
      3    a = 0
      4    for i in my_list:
----> 5        a += i
      6    return a

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

Here, we have defined a function fail() that steps through an input list, which ideally should be all int objects, and returns the sum of the list. However, our input list contains a str object, so our function crashes when it loops over this object. This is described in the traceback that points at the line a += 1 along with the error message TypeError: unsupported operand type(s) for +=: 'int' and 'str'. Since the code is trying to add an int object to a str object (i.e., when we get to "3" in our list), a TypeError exception type is flagged.

The try - except pattern will help us bypass this failure mode in our code. The try - except pattern is similar in structure to the if - else coding pattern that was described in an earlier lesson. To use the try - except pattern, we first start with the try: command, followed by the code block that we want to try to execute. After that, we use the except: command, followed with the code block that will be executed if there is a problem in the try code block. Below is a simplified version of the previously failed code that implements the try - except pattern:

def fail(my_list):
   """Tallies up total from a list."""
   a = 0
   try:
       for i in my_list:
           a += i
   except Exception as e:
       print(e)
   return a

# tally up total from provided list
b = fail([1, 2, "3", 4])
# print total
print("b:", b)
unsupported operand type(s) for +=: 'int' and 'str'
b: 3

You can see we got to the end of the code, and we were able to print out the value of b. Without the try - except pattern our code would have crashed out during the iteration loop that set i to "3". Instead, the except: code block was triggered and printed the message unsupported operand type(s) for +=: 'int' and 'str' instead of crashing. It then proceeded to the next try: code block with i set to 4 and executed this command successfully. Therefore, the next except: block is skipped. The end result is that b is set to 3, is its value up to the error iteration.

So we gained more information! If we want to make sure all elements of the list is processed, we need to put the try - except pattern inside the for loop:

def fail(my_list):
   """Tallies up total from a list."""
   a = 0
   failed = []
   
   for i in my_list:
       try:
           a += i
       except Exception as e:
           print(e, i)
           failed.append(i)
   return a, failed

# tally up total and records any errors from the provided list
b, failures = fail([1, 2, "3", 4])
# print total and possible errors
print("b:", b)
print("failed:", failures)
unsupported operand type(s) for +=: 'int' and 'str' 3
b: 7
failed: ['3']

Now we are able to get b equal to 7 by summing up 1, 2, and 4. Additionally, we added a failed list and when the except block is executed we append the failed element to that list. We can now return a and failed as output of the function.

Modify the solution from the previous example using the try - except command structure so the code iterates over all values in the starting list and also mentions if a number in the list is less than zero.


Solution:

Very little needs to be done in order to add the try - except command structure. The try command will have our main print() code, while the except command issues a new print() call that states that the current value in the list is less than zero.

from math import sqrt

list = [2, 4, 16, -5, 253, -2.5, 100]

for i in list:
    try:
        print(f"The square root of {i} is {sqrt(i):.3f}.")
    except:
        print(f"Cannot take the square root of {i} since it is less than zero!")
The square root of 2 is 1.414.
The square root of 4 is 2.000.
The square root of 16 is 4.000.
Cannot take the square root of -5 since it is less than zero!
The square root of 253 is 15.906.
Cannot take the square root of -2.5 since it is less than zero!
The square root of 100 is 10.000.

All in all, the try - except command structure is an effective technique to ensure that erroneous values are appropriately flagged while ensuring the program completes.


While we can use try - except to bypass errors, there are times when we want a program to exit when an error is encountered and return more information to the user besides a generic exception message. The raise keyword is designed to exit the code with a raised exception that can include additional information for the user as to why the program exited. The code below demonstrates how the raise keyword is used:

def fail(my_list):
   a = 0
   for i in my_list:
       if isinstance(i, int):
         a += i
       else:
         raise Exception("Value in list is not an integer: " + str(i))
   return a
   
b = fail([1, 2, "3", 4])
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[7], line 10
      6        else:
      7          raise Exception("Value in list is not an integer: " + str(i))
      8    return a
      9 
---> 10 b = fail([1, 2, "3", 4])

Cell In[7], line 7, in fail(my_list)
      3    for i in my_list:
      4        if isinstance(i, int):
      5          a += i
      6        else:
----> 7          raise Exception("Value in list is not an integer: " + str(i))
      8    return a

Exception: Value in list is not an integer: 3

In this example, a new built-in function is being used in the if statement, isinstance(), which compares the class of an object to a desired class. In this case, we are testing to see if i belongs to the int class. If it is an int object, the program moves the following code block with the a += i command. If it is not an int object, then the program moves to the else: code block and raises an exception.

As seen above, the raise command allows us to insert our own unique exception (in this case "Value in list is not an integer: " + str(i)) ). If we did not include this but rather used our except Exception as e structure from before we would have received the generic traceback exception message: TypeError: unsupported operand type(s) for +=: 'int' and 'str'. Therefore, the raise keyword has allowed us to produce a more informative error message that clearly says that an element in the list is not an integer and the erroneous value in question.

There are many built-in Exceptions, and we can make custom ones too. It is a good practice to match the exception type to what sort of error is happening. For example, we can use the built-in ValueError exception which is better suited for an exception type than the generic Exception:

def fail(my_list):
   a = 0
   for i in my_list:
       if isinstance(i, int):
         a += i
       else:
         raise ValueError("Value in list is not an integer: " + str(i))
   return a
   
b = fail([1, 2, "3", 4])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 10
      6        else:
      7          raise ValueError("Value in list is not an integer: " + str(i))
      8    return a
      9 
---> 10 b = fail([1, 2, "3", 4])

Cell In[8], line 7, in fail(my_list)
      3    for i in my_list:
      4        if isinstance(i, int):
      5          a += i
      6        else:
----> 7          raise ValueError("Value in list is not an integer: " + str(i))
      8    return a

ValueError: Value in list is not an integer: 3

9.5.1Example: Raising an issue

An example in a previous lesson created a function that converted an input force to one of three units. Part of the exercise required the function to notify if incorrect force units were used. This was done using simple print() calls. In this example, replace the print() calls with raise commands so ValueError exceptions are issued when incorrect units are entered.


Solution:

Since the print() calls that handle the incorrect units are placed in else statements, it is straightforward to modify the code. All we need to do is replace the print() functions with a raise ValueError() calls.

def convert_force(initial_force, initial_units="N", converted_units="lbf"):
    """
    Convert force between newtons, pound force, and dynes. Returns the 
    converted force and units.

    Parameters
    ----------
    initial_force : float
        Force to be converted.

    initial_units : str
        Initial units of force. Allowable values are "N", "lbf", and "dyn". 
        Default value is "N"

    converted_units : str
        Units of force to be converted to. Allowable values are "N", "lbf", 
        and "dyn". Default value is "N"

    Returns
    -------
    converted_force : float 
        Converted force value.

    converted_units : str
        Units of converted force. Default value is "lbf".
    """
    
    # Convert force to N to standardize
    if initial_units == "N":
        force_N = initial_force
    elif initial_units == "lbf":
        force_N = initial_force / 0.225
    elif initial_units == "dyn":
        force_N = initial_force / 100000
    else:
        raise ValueError("Incorrect initial force units")

    # Convert to new units
    if force_N == "\"incorrect initial force units\"":
        converted_force = force_N
    elif converted_units == "N":
        converted_force = force_N
    elif converted_units == "lbf":
        converted_force = force_N * 0.225
    elif converted_units == "dyn":
        converted_force = force_N * 100000
    else:
        raise ValueError("Incorrect converted force units")
    
    return converted_force, converted_units

The code below demonstrates the error handling in action:

converted_force, converted_units = convert_force(2536.2, 
                                                initial_units="dyn", 
                                                converted_units="bad")

print(f"The converted force is {converted_force} {converted_units}.")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[10], line 1
----> 1 converted_force, converted_units = convert_force(2536.2, 
      2                                                 initial_units="dyn",
      3                                                 converted_units="bad")
      4 

Cell In[9], line 48, in convert_force(initial_force, initial_units, converted_units)
     44         converted_force = force_N * 0.225
     45     elif converted_units == "dyn":
     46         converted_force = force_N * 100000
     47     else:
---> 48         raise ValueError("Incorrect converted force units")
     49 
     50     return converted_force, converted_units

ValueError: Incorrect converted force units

This route is a more effective way for error handling since it produces both the exception output block and the traceback.


9.6Documentation and troubleshooting

It can be difficult to troubleshoot errors in code. Besides the inherent complexity of the code you are working with, Python libraries are constantly in development and changing. However, there are a few general strategies we can use to troubleshoot coding errors:

9.6.1Use help()

As we have discussed in an earlier lesson, the built-in help() function is a useful function to read the docstrings of functions. Information about default values, normal behavior, and commonly seen errors are often included in these docstrings.

9.6.2Search online by last traceback line

A simple search online of the last traceback line can help, especially if the last line is from a Python library file. When searching the traceback, you may find other users that have encountered the problem. A popular site for asking questions and getting answers is StackOverflow. In evaluating an answer, it is useful to keep your operating environment in mind. Are the answers for Python 2.7 or for Python 3.10? Are they for your operating system or for a different one? How old is the answer, does it apply to our library version?

9.6.3Check the library API documentation online

Most Python libraries have their documentation published online. We can search online for “LIBRARY NAME documentation” to find the documentation site and check to see if we are using a library or function correctly. You may also be able to select the specific version of the library you are using. For instance, on the popular documentation hosting site Read the Docs, the version of the library will be in the bottom left.

As an example of checking documentation for correct usage, the numpy.loadtxt() documentation shows the many functional changes across the different versions. For instance, the quotechar optional argument was new in version 1.23.0. If we are running code that uses that argument, and our version of NumPy is older than 1.23.0, we will run into an error (the installed version of NumPy can be checked with numpy.version.full_version).

9.6.4Check library “Getting Started” or “Tutorials”

If you are getting an error with a library you are using for the first time, it can be useful to run known working code as a sanity check with your setup. For example, scikit-image is a bundled library with Anaconda and is useful for image processing. We can try out the robust gallery of examples first to make sure the functions we are using in scikit-image are working.

9.7Final thoughts

Bugs and errors are part of life when coding, and it can be disheartening when we run into them. But with the useful tracebacks that Python generates, we can see how an exception was raised through step by step analysis. We can also use the try-except coding pattern to work around some errors, such as bypassing bad inputs from a file. Sometimes it is important to raise an exception in our code, which will stop execution, but can provide important contextual information through raise statements. Finally, looking for solutions through online resources is a great way to get a head start on correcting an error. Keep in mind your operating environment when evaluating different answers online, as some answers may not apply to your particular situation. Searching through the documentation online and trying out different tutorials is a great way to sanity check your current code.