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.
One of the most important aspects of programming is learning how to deal with errors and bugs in code.
Types of bugs/errors in code, from the easiest to the most difficult to diagnose:
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 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 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?
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.
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.
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..."
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:
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.
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
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.
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.
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:
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 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:
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.
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.