Error Handling

From Jake Vanderplas' Exceptions and Debugging lesson.


Instructions: Create a new directory called ErrorHandling with a notebook called ErrorHandlingTour . Give it a heading 1 cell title Error handling. Read this page, typing in the code in the code cells and executing them as you go.

Do not copy/paste.

Type the commands yourself to get the practice doing it. This will also slow you down so you can think about the commands and what they are doing as you type them.</font>

Save your notebook when you are done, then try the accompanying exercises.


This tour will provide information that will help you write clean, usable, and maintainable code. Working with Python in science is much more than simply the mechanics of writing code: it's about writing code in a way that allows it to be used, re-used, adapted, and understood by collaborators and your future self.

Errors, Exceptions, and Debugging

One of the most important aspects of programming is learning how to deal with errors and bugs in code.

Three Classes of Errors:

Types of bugs/errors in code, from the easiest to the most difficult to diagnose:

  1. Syntax Errors: Errors where the code is not valid Python (generally easy to fix)
  2. Runtime Errors: Errors where syntactically valid code fails to execute (sometimes easy to fix)
  3. Semantic Errors: Errors in logic (often very difficult to fix)

Syntax Errors

You've probably already seen lots of these. Syntax errors are when you write code which is not valid Python. For example:


In [ ]:
X = [1, 2, 3)

In [ ]:
y = 4x + 3

Note that if your code contains even a single syntax error, none of it will run:


In [ ]:
a = 4
something ==== is wrong

In [ ]:
print a

Even though the syntax error appears below the (valid) variable definition, the valid code is not executed.


Runtime Errors

Runtime errors occur when the code is valid python code, but there are errors within the context of the program execution. For example:


In [ ]:
print Q

In [ ]:
x = 1 + 'abc'

In [ ]:
X = 1 / 0

In [ ]:
import numpy as np
np.add(1, 2, 3, 4)

In [ ]:
x = [1, 2, 3]
print x[100]

Unlike Syntax errors, RunTime errors occur during code execution, which means that valid code occuring before the runtime error will execute:


In [ ]:
spam = "my all-time favorite"
eggs = 1 / 0

In [ ]:
print spam

Semantic Errors

Semantic errors are perhaps the most insidious errors, and are by far the ones that will take most of your time. Semantic errors occur when the code is syntactically correct, but produces the wrong result.

By way of example, imagine you want to write a simple script to approximate the value of $\pi$ according to the following formula:

$$ \pi = \sqrt{12}\sum_{k=0}^{\infty}\frac{(-3)^k}{2k+1} $$

You might write a function something like this, using NumPy's vectorized syntax:


In [ ]:
def approx_pi(nterms=100):
    k = np.arange(nterms)
    return np.sqrt(12) ** np.sum((-3.0) ** (-k) / (2.0 * k + 1.0))

Looks OK, yes? Let's try it out:


In [ ]:
print np.sqrt(12)

In [ ]:
print approx_pi(100)

Huh. That doesn't look like $\pi$. Maybe we need more terms?


In [ ]:
print approx_pi(1000)

Nope... it looks like the algorithm simply gives the wrong result. This is a classic example of a semantic error.

Question: can you spot the problem?


Runtime Errors and Exception Handling

Now we'll talk about how to handle RunTime errors (we skip Syntax Errors because they're pretty self-explanatory).

Runtime errors can be handled through "exception catching" using try...except statements. Here's a basic example:


In [ ]:
try:
    print "this block gets executed first"
except:
    print "this block gets executed if there's an error"

In [ ]:
try:
    print "this block gets executed first"
    x = 1 / 0  # ZeroDivisionError
    print "we never get here"
except:
    print "this block gets executed if there's an error"

Notice that the first block executes up until the point of the Runtime error. Once the error is hit, the except block is executed.

One important note: the above clause catches any and all exceptions. It is not generally a good idea to catch-all. Better is to name the precise exception you expect:


In [ ]:
def safe_divide(a, b):
    try:
        return a / b
    except:
        print "oops, dividing by zero. Returning None."
        return None
    
print safe_divide(15, 3)
print safe_divide(1, 0)

But there's a problem here: this is a catch-all exception, and will sometimes give us misleading information. For example:


In [ ]:
print safe_divide(15, 'three')

Our program tells us we're dividing by zero, but we aren't! This is one reason you should almost never use a catch-all try...except statement, but instead specify the errors you're trying to catch:


In [ ]:
def better_safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as err:
        print err
        print "oops, dividing by zero. Returning None."
        return None
    
print better_safe_divide(15, 0)

In [ ]:
print better_safe_divide(15, 'three')

This also allows you to specify different behaviors for different exceptions:


In [ ]:
def even_better_safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as err:
        print err
        print "oops, dividing by zero. Returning None."
        return None
    except TypeError as err:
        print err
        print "incompatible types.  Returning None"
        return None
    
print even_better_safe_divide(15, 3)
print even_better_safe_divide(15, 0)
print even_better_safe_divide(15, 'three')

Remember this lesson, and always specify your except statements! You could spend an entire day tracing down a bug in your code which amounts to something as trivial as this.


Raising Your Own Exceptions

When you write your own code, it's good practice to use the raise keyword to create your own exceptions when the situation calls for it:


In [ ]:
import os # the "os" module has useful operating system stuff

def read_file(filename):
    if not os.path.exists(filename):
        raise ValueError("'%s' does not exist"%format(filename))
    f = open(filename)
    result = f.read()
    f.close()
    return result

In [ ]:
%%file tmp.txt
this is the contents of the file

In [ ]:
read_file('tmp.txt')

In [ ]:
read_file('file.which.does.not.exist')

It is sometimes useful to define your own custom exceptions, which you can do easily via class inheritance:


In [ ]:
class NonExistentFile(RuntimeError):
    # you can customize exception behavior by defining class methods.
    # we won't discuss that here.
    pass

def read_file(filename):
    if not os.path.exists(filename):
        raise NonExistentFile(filename)
    f = open(filename)
    result = f.read()
    f.close()
    return result

In [ ]:
print read_file('tmp.txt')

In [ ]:
print read_file('file.which.does.not.exist')

Python has a whole class of built-in error types that you can use for standard exceptions. For example in the previous situation we could catch an IOError exception:


In [ ]:
def read_file(filename):
    try:
       f = open(filename)
       result = f.read()
       f.close()
       return result
    except IOError as err:
       print err
       print "File does not exist: ",err.filename
       return None

In [ ]:
print read_file('file.which.does.not.exist')

Get used to throwing appropriate — and meaningful — exceptions in your code! It makes reading and debugging your code much, much easier.


More Advanced Exception Handling

There is also the possibility of adding else and finally clauses to your try statements. The behavior looks like this:


In [ ]:
try:
    print "doing something"
except:
    print "this only happens if it fails"
else:
    print "this only happens if it succeeds"

In [ ]:
try:
    raise ValueError()
except:
    print "this only happens if it fails"
else:
    print "this only happens if it succeeds"

Why would you ever want to do this? Mainly, it prevents the code within the else block from being caught by the try block. Accidentally catching an exception you don't mean to catch can lead to confusing results.


In [ ]:
try:
    print "do something"
except:
    print "this only happens if it fails"
else:
    raise ValueError()

The last statement you might use is the finally statement, which looks like this:


In [ ]:
try:
    print "do something"
except:
    print "this only happens if it fails"
else:
    print "this only happens if it succeeds"
finally:
    print "this happens no matter what."

In [ ]:
try:
    raise ValueError()
except:
    print "this only happens if it fails"
else:
    print "this only happens if it succeeds"
finally:
    print "this happens no matter what."

finally is generally used for some sort of cleanup (closing a file, etc.) It might seem a bit redundant, though. Why not write the following?


In [ ]:
try:
    print "do something"
except:
    print "this only happens if it fails"
else:
    print "this only happens if it succeeds"
print "this happens no matter what."

The main difference is when the clause is used within a function:


In [ ]:
def divide(x, y):
    try:
       result = x / y
    except ZeroDivisionError as err:
        print err
        return None
    else:
        print "result is", result
        return result
    finally:
        print "some sort of cleanup"

In [ ]:
print divide(15, 3)

In [ ]:
print divide(15, 0)

Note that the finally clause is executed no matter what, even if the return statement has already executed! This makes it useful for cleanup tasks, such as closing an open file, restoring a state, or something along those lines.


In [ ]:
while True:
    try:
        x = int(raw_input("Please enter a whole number: "))
        break
    except ValueError:
        print "Oops!  That was no valid number.  Try again..."

Handling Semantic Errors: Debugging

Here is the most difficult piece of this tour: handling semantic errors. This is the situation where your program runs, but doesn't produce the correct result. These errors are commonly known as bugs, and the process of correcting the bugs is debugging.

There are three main methods commonly used for debugging Python code. In order of increasing sophistication, they are:

  • Inserting print statements
  • Injecting an IPython interpreter
  • Using a line-by-line debugger like pdb

The easiest method: print statements

Say we're trying to compute the entropy of a set of probabilities. The form of the equation is

$$ H = - \sum_i p_i \log(p_i) $$

We can write the function like this:


In [ ]:
def entropy(p):
    p = np.asarray(p)  # convert p to array if necessary
    items = p * np.log(p)
    return -np.sum(items)

Say these are our probabilities:


In [ ]:
p = np.arange(5.)
p /= p.sum()
print p

In [ ]:
print entropy(p)

We get nan, which stands for "Not a Number". What's going on here?

Often the first thing to try is to simply print things and see what's going on. Within the file, you can add some print statements in key places:


In [ ]:
def entropy(p):
    p = np.asarray(p)  # convert p to array if necessary
    print p
    items = p * np.log(p)
    print items
    return -np.sum(items)

entropy(p)

By printing some of the intermediate items, we see the problem: 0 * np.log(0) is resulting in a nan. Though mathematically it's true that $\lim_{x\rightarrow0}[x\log(x)]=0$, the fact that we're performing the computation numerically means that we don't obtain this result.

Often, inserting a few print statements can be enough to figure out what's going on.

I will mention the more sophisticated debugging methods briefly here, but we probably won't resort to using them in this class. They become critical when you are working on very large (>1000 lines) codes that are hard to troubleshoot with simple print statements unless you can isolate the erroneous behavior to a small region of the code.

Embedding an IPython instance

You can go a step further by actually embedding an IPython instance in your code. This doesn't work from within the notebook, so we'll create a file and run it from the command-line


In [ ]:
%%file test_script.py
import numpy as np

def entropy(p):
    p = np.asarray(p)  # convert p to array if necessary
    items = p * np.log(p)
    import IPython; IPython.embed()
    return -np.sum(items)

p = np.arange(5.)
p /= p.sum()
entropy(p)

Now open a terminal and run this with

python test_script.py

You'll see that an IPython command-line interpreter opens, and from there you can print p, print items, and do any manipulation you feel like doing (this is equivalent to code cells within the notebook). This can also be a nice way to debug a script. To exit the iPython interpreter, type quit or exit:

ipython
In [1]: quit

Using a Debugger

Python comes with a built-in debugger called pdb. It allows you to step line-by-line through a computation and examine what's happening at each step. Note that this should probably be your last resort in tracing down a bug. But it can be a useful tool to have in your toolbelt.


Testing

It's pretty obvious that if we want to be sure our programs are right, we need to put in some effort. What isn't so obvious is that focusing on quality is also the best way—in fact, the only way—to improve productivity as well. Getting something wrong and then fixing it almost always takes longer than getting it right in the first place. Designing testable code, practicing defensive programming, writing and running tests, and thinking about what the right answer is supposed to be all help get us answers faster, as well as ones that are more likely to be correct.

  • Testing can't find all mistakes, any more than proof-reading can find all typos, but both are still useful.
  • Use exceptions to report and handle errors: throw low, catch high.
  • Use an xUnit library to manage unit tests in a uniform, predictable way.
  • Isolating components for testing also improves code quality.
  • Use approximate comparisons when dealing with floating point numbers.
  • Separate test setup and teardown from test execution.

Study after study has shown that the more you invest up front in quality, the sooner your program will be ready to use. Just as in manufacturing and medicine, slowing down a little is the best way to speed things up a lot.

Testing actually serves two purposes. It tells you whether your program is doing what it's supposed to do. But if it's done right, it will also tell you what your program actually is supposed to be doing.

Tests are runnable specifications of a program's behavior.

Unlike design documents or comments in the code, you can actually run your tests, so it's harder for them to fall out of sync with the program's actual behavior. In well-run projects, tests also act as examples to show newcomers how the code should be used, and how it's supposed to behave under different circumstances.

It's important to understand that there's a lot more to software quality than testing. Testing doesn't create quality, it measures it. As Steve McConnell (author of many software engineering textbooks) said, trying to improve the quality of software by doing more testing is like trying to lose weight by weighing yourself more often.

But a good set of tests will help you track down bugs more quickly, which in turn speeds up development.

It's also important to understand that testing can only do so much. For example, suppose you're testing a function that compares two 7-digit phone numbers. There are 10$^7$ such numbers, which means that there are 10$^{14}$ possible test cases for your function. At a million tests per second, it would take you 155 days to run them all.

And that's only one simple function: exhaustively testing a real program with hundreds or thousands of functions, each taking half a dozen arguments, would take many times longer than the expected lifetime of the universe. And how would you actually write 10$^{14}$ tests? More importantly, how would you check that the tests themselves were all correct?

In reality, "all" that testing can do is show that there might be a problem in a piece of code. If testing doesn't find a failure, there could still be bugs lurking there that we just didn't find. And if testing says there is a problem, it could well be a problem with the test rather than the program.

So why test? Because it's one of those cases where something that shouldn't work in theory is surprisingly effective in practice. It's just like mathematics: any theorem proof might contain a flaw that just hasn't been noticed yet, but somehow we manage to make progress.

The obstacle to testing isn't actually whether or not it's useful, but whether or not it's easy to do. If it isn't, people will always find excuses to do something else.

It's therefore important to make things as painless as possible. In particular, it has to be easy for people to:

  • add or change tests
  • understand existing tests
  • run tests
  • understand test results

And test results must be reliable.

If a testing tool says that code is working when it's not, or reports problems when there actually aren't any, people will lose faith in it and stop using it. No false positives or false negatives.

Assertions

Assertions are a systematic way to check that the internal state of a program is as the programmer expected, with the goal of catching bugs. In particular, they're good for catching false assumptions that were made while writing the code, or abuse of an interface by another programmer. In addition, they can act as in-line documentation to some extent, by making the programmer's assumptions obvious. ("Explicit is better than implicit.")

Python's assert statement helps you find bugs more quickly and with less pain.

How does it work?

assert checks a condition, if it is True it does nothing, and if it is False it raises an AssertionError with an optional error message. For example:


In [ ]:
x = 23
assert x > 0, "x is not zero or negative"
assert x%2 == 0, "x is not an even number"

Places to consider putting assertions:

  • checking parameter types, classes, or values
  • checking data structure invariants
  • checking "can't happen" situations (duplicates in a list, contradictory state variables.)
  • after calling a function, to make sure that its return is reasonable

The overall point is that if something does go wrong, we want to make it completely obvious as soon as possible.

It's easier to catch incorrect data at the point where it goes in than to work out how it got there later when it causes trouble.

Summary

In this tour we explored some of the causes of errors in our code and looked at some ways to handle them. We did not do an in-depth exploration of standard practices for testing code but I tried to give you one example for how to go about it. If you are interested in learning more, there are lots of online resources to help you.


All content is under a modified MIT License, and can be freely used and adapted. See the full license text here.