Building better programs in Python (Part 4)
Detecting errors
Let's take a look at a few examples of error messages.
Example 1
Consider the code
def not_zero(value):
return not value = 0
def my_function(first, second):
if notzero(first):
return first
return second
print(my_function(0, 1))
In this example, the helper function not_zero
returns True
if the value is not zero. The function my_function
returns the first parameter if it is not zero and otherwise returns the second parameter. On running the code, we get the output
Traceback (most recent call last): File "<string>", line 2 return not value = 0 ^ SyntaxError: invalid syntax
The error message shows that we have a syntax error and where the error occurred. It's one of those common errors: =
instead ==
. We fix it:
def not_zero(value):
return not value == 0
def my_function(first, second):
if notzero(first):
return first
return second
print(my_function(0, 1))
This time we have a NameError:
Traceback (most recent call last): File "<string>", line 9, in <module> File "<string>", line 5, in my_function NameError: name 'notzero' is not defined
Why do we get a NameError That's because of a typing error: we have typed notzero
instead of not_zero
. We fix it:
def not_zero(value):
return not value == 0
def my_function(first, second):
if not_zero(first):
return first
return second
print(my_function(0, 1))
Now the function works properly, giving the output 0.
Example 2
Consider the code
import math
val_1 = 10
val_2 = 20
val_3 = 30
def my_function(first, second, third):
return math.PI * third + first + second
print(my_function("val_1", "val_2", "val_3"))
In this example, our function returns the product of π and the third input added to the first and second inputs. On running the code, we get the error
Traceback (most recent call last): File "<string>", line 10, in <module> File "<string>", line 8, in my_function AttributeError: 'module' object has no attribute 'PI'
We have an attribute error telling us that there is no attribute 'PI'. This is another typing error, but yields a different error message than the error message we received for the previous typing error. We fix it:
import math
val_1 = 10
val_2 = 20
val_3 = 30
def my_function(first, second, third):
return math.pi * third + first + second
print(my_function("val_1", "val_2", "val_3"))
We run it and now we get a TypeError:
Traceback (most recent call last): File "<string>", line 10, in <module> File "<string>", line 8, in my_function TypeError: can't multiply sequence by non-int of type 'float'
The TypeError is telling us that we can't multiply a sequence by a non-int of type float. We don't need to know what a sequence is to see that the problem is in multiplying π by third. Why should that be a problem? To figure it out, we trace back to see what the value is. Now we see the problem: we used strings as inputs to the function instead of variable names. We fix it:
import math
val_1 = 10
val_2 = 20
val_3 = 30
def my_function(first, second, third):
return math.pi * third + first + second
print(my_function(val_1, val_2, val_3))
We now have a working function and get the output 124.24777960769379.
Example 3
In our final example, we have a helper function that returns first
if it is smaller than second
, and then uses the helper function repeatedly in my_function
, first to obtain the variable one
, then to obtain the variable two
, and then in combination with these variables to obtain the variable three
:
def smaller(first, second):
if first < second:
return first
def my_function(first, second, third):
one = smaller(first, second)
two = smaller(second, third)
three = one + two - smaller(first, third)
return three
print(my_function(10, 9, 8))
Running the function gives the output
Traceback (most recent call last): File "<string>", line 11, in <module> File "<string>", line 8, in my_function TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
The error message tells us that we can't add values of type None
:
Why does it say that? We are adding one
and two
. What types are one
and two
? The function smaller
returns one of the parameters. The parameters are integers. So why aren't one
and two
integers? Let's add print
statements to see what is going on:
def smaller(first, second):
if first < second:
return first
def my_function(first, second, third):
one = smaller(first, second)
print(one)
two = smaller(second, third)
print(two)
three = one + two - smaller(first, third)
return three
print(my_function(10, 9, 8))
We get the following out put when the code is run:
None None Traceback (most recent call last): File "<string>", line 13, in <module> File "<string>", line 10, in my_function TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
Both variables, one
and two
, have the value None
. So let's look more carefully at the helper function smaller
. Why wasn't first
returned? Now we've found the problem: first
is only returned when first
is less than second
. Since 10 is not less than 9, in that case smaller
didn't return a value, which is why one had the value None
. We can fix the function:
def smaller(first, second):
if first < second:
return first
return second
def my_function(first, second, third):
one = smaller(first, second)
print(one)
two = smaller(second, third)
print(two)
three = one + two - smaller(first, third)
return three
print(my_function(10, 9, 8))
Now we have no more error messages and get the output
9 8 9
Our final step is to remove the print statements used for debugging:
def smaller(first, second):
if first < second:
return first
return second
def my_function(first, second, third):
one = smaller(first, second)
two = smaller(second, third)
three = one + two - smaller(first, third)
return three
print(my_function(10, 9, 8))
Now we get the output 9 when the code is run.
Common runtime errors
- NameError - no such name has been defined
- Check for typos.
- Check where the name was defined.
- AttributeError - no such attribute exists
- Check for typos.
- Make sure you are returning a value.
- TypeError - data is of the wrong type
- IndexError - no value at this index
- ZeroDivisionError - dividing by zero
To summarize the errors we've seen and what to do about them, if we see a NameError we know that there is a name that hasn't been defined. This might be a typo or it might mean that we haven't defined the variable that we're using. For example, maybe it was defined only inside a temporary address book, but not where we are trying to use it. Similarly, for an AttributeError, we can check for typos or to make sure that a value is being returned. A TypeError means that data is of the wrong type. This requires figuring out what type is expected and what type is being provided. An IndexError occurs when, for example, the value of the index is larger than the length of the string. Then there is the self-descriptive ZeroDivisionError. These aren't all the error messages you might see, but they give you an idea of the ways to handle them.
Testing in Python
To test in Python we'll use assertions:
An assertion contains a Boolean expression.
An assertion error occurs if the value of the expression is False
.
Let's illustrate this using the ordinal example that we wrote earlier.
def ordinal(num):
"""Docstring here.
"""
## Convert integer to a string
root = str(num)
## Determine the ending
if num % 10 == 1:
ending = "st"
elif num % 10 == 2:
ending = "nd"
elif num % 10 == 3:
ending = "rd"
else:
ending = "th"
## Concatenate the two parts
return root + ending
This is the function that consumes an integer and produces a string with the ordinal, which we wrote earlier.
def test_ordinal():
assert ordinal(21) == "21st"
assert ordinal(642) == "642nd"
assert ordinal(53) == "53rd"
assert ordinal(21325) == "21325th"
test_ordinal()
We can create and run a special testing function. In the function, each assertion consists of a Boolean expression with a function call and the value we expect to be produced. If the function works properly, nothing will be returned.
Caution
Use assertions to check values produced by functions, not printed.
Since an assertion is checking whether two values are equal, it works well for checking what a function call produces. This is not the same as checking what is printed, which cannot be checked in this way. We will make use of assertions in our testing.