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.