9 Understanding Errors
9.1Lesson goals¶
Read and understand error messages.
Use error messages to debug code.
Create custom error messages with the
raisecommand.
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 definedIn 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 asdfThe traceback then prints out the exception type and the error message:
NameError: name 'asdf' is not definedHere, 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 defined9.3.1Example: The wrong value¶
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:
, , , , , , and
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, and 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 errorAs 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.
9.4.1Example: Trying to get out of the problem¶
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.
9.5Returning errors with raise¶
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: 3In 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: 39.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_unitsThe 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 unitsThis 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.