In [ ]:
import numpy as np

When Things Go Wrong:

Exceptions and Errors

Today we'll cover perhaps one of the most important aspects of using Python: dealing 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

Syntax errors are when you write code which is not valid Python. For example:


In [3]:
X = [1, 2, 3
a = 4


  File "<ipython-input-3-dbcc3d32bbf7>", line 2
    a = 4
    ^
SyntaxError: invalid syntax

In [ ]:
y = 4*x + 3

In [4]:
def f():
    return GARBAGE

No error is generated by defining this function. The error doesn't occur until the function is called.


In [5]:
f()


-------------------------------------------------------------------------
NameError                               Traceback (most recent call last)
<ipython-input-5-c43e34e6d405> in <module>()
----> 1 f()

<ipython-input-4-309043ae9eb7> in f()
      1 def f():
----> 2     return GARBAGE

NameError: name 'GARBAGE' is not defined

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 are errors within the context of the program execution. For example:


In [ ]:
print(Q)

In [6]:
a = 'aaa'

# blah blah blah
x = 1 + int(a)
print (x)


-------------------------------------------------------------------------
ValueError                              Traceback (most recent call last)
<ipython-input-6-4e46bd5be9f3> in <module>()
      2 
      3 # blah blah blah
----> 4 x = 1 + int(a)
      5 print (x)

ValueError: invalid literal for int() with base 10: 'aaa'

In [7]:
X = 1 / 0


-------------------------------------------------------------------------
ZeroDivisionError                       Traceback (most recent call last)
<ipython-input-7-3bbf9829bc52> in <module>()
----> 1 X = 1 / 0

ZeroDivisionError: division by zero

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


Out[11]:
10

In [10]:
np.sum?

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 [ ]:
total = 0
for k in ks:
    total += (-3.0) ** -k / (2 * k + 1)

In [18]:
from math import sqrt

def approx_pi(nterms=100):
    ks = np.arange(nterms)
    ans = sqrt(12) * np.sum([-3.0 ** -k / (2 * k + 1) for k in ks])
    if ans < 1:
        import pdb; pdb.set_trace()
    return ans
approx_pi(1000)


Out[18]:
-3.9508736907744493

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


In [ ]:
approx_pi(1000)

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


In [ ]:
k = 2
(-3.0) ** -k / (2 * k + 1)

In [ ]:
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 [24]:
try:
    print("this block gets executed first")
    GARBAGE
except (ValueError, RuntimeError) as err:
    print("this block gets executed if there's an error")
    print (err)
print ("I am done!")


this block gets executed first
-------------------------------------------------------------------------
NameError                               Traceback (most recent call last)
<ipython-input-24-5d2b8d11a118> in <module>()
      1 try:
      2     print("this block gets executed first")
----> 3     GARBAGE
      4 except (ValueError, RuntimeError) as err:
      5     print("this block gets executed if there's an error")

NameError: name 'GARBAGE' is not defined

In [ ]:
def f(x):
    if isinstance(x, int) or isinstance(x, float):
        return 1.0/x
    else:
        raise ValueError("argument must be an int.")

In [ ]:
f(0)

In [ ]:
def f(x):
    try:
        return 1.0/x
    except (TypeError, ZeroDivisionError):
        raise ValueError("Argument must be a non-zero number.")

In [ ]:
f(0)

In [ ]:
f('aa')

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 [26]:
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))


5.0
None

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


In [ ]:
safe_divide(15, 3)

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:
        print("oops, dividing by zero. Returning None.")
        return None
    
better_safe_divide(15, 0)

In [ ]:
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:
        print("oops, dividing by zero. Returning None.")
        return None
    except TypeError:
        print("incompatible types.  Returning None")
        return None

In [ ]:
even_better_safe_divide(15, 3)

In [ ]:
even_better_safe_divide(15, 0)

In [ ]:
even_better_safe_divide(15, 'three')

Remember this lesson, and always specify your except statements! I once spent an entire day tracing down a bug in my code which amounted to 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("'{0}' does not exist".format(filename))
    f = open(filename)
    result = f.read()
    f.close()
    return result

We'll use IPython's %%file magic to quickly create a text file


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 result4o-

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

In [ ]:
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. You'll probably not need these often, but in case you encounter them some time, it's good to know what they do.

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:
    print("doing something")
    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.

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:
    print("do something")
    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.")

Write exception code that handles all exceptions except ZeroDivideError


In [ ]:
x = 0
    excpt = None
    try:
        1.0/x
    except Exception as err:
        if isinstance(err, ZeroDivisionError):
            pass
        else:
            raise Exception("Got error.")

In [ ]:
type(excpt)

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


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

In [ ]:
divide(15, 3)

In [ ]:
divide(15, 0)