Exceptions and tracebacks

Error handling and debugging are major aspects of programming. Python provides the Exception base class and a set of built-in derived classes for reporting "exceptional" conditions, which might be genuine errors, or simply cases to be handled by an alternative code path. It also provides syntax for raising Exceptions, and for catching or trapping them so that they can be handled by additional code.

Note: in this notebook we are deliberately generating errors in the sample code, so you will not be able to use the "Cell: Run All" notebook menu option to run the whole notebook at once. Execution will stop as soon as the first uncaught Exception is raised.

Let's start with a very simple example of an error that raises an Exception, leading IPython to print a traceback:

In [1]:
def sumsq(*args):
    out = 0
    for x in args:
        out = out + x ** 2
    return out

print(sumsq(1, 2, "3"))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-9316afc0c91a> in <module>()
      5     return out
      6 
----> 7 print(sumsq(1, 2, "3"))

<ipython-input-1-9316afc0c91a> in sumsq(*args)
      2     out = 0
      3     for x in args:
----> 4         out = out + x ** 2
      5     return out
      6 

TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

The erroneous input--a string where a number was needed--caused an Exception to be raised. An Exception is a particular type of class or an instance of such a class, and a TypeError is one of several built-in sub-classes of Exception. The error message is called a Traceback because it traces the chain of function calls that led to the Exception. In this case there are only two steps in the Traceback: first the print statement called the sumsq function, and then the sumsq function tried to perform an arithmetic operation. Tracebacks are always printed in this order: the innermost function--the one that first ran into trouble--is at the bottom, and above that is whatever called that function, and so forth up to the top.

When troubleshooting, one therefore starts from the bottom of the traceback, and works back up the stack, looking for the place where the real error occurred--the place where something needs to be fixed. In this example, the problem is not that there is something wrong with line out = out + arg **2, but that the prior print statement provided an invalid value when calling the sumsq() function.

Sometimes it is useful to anticipate such errors and trap them, either to provide a workaround, or to generate a custom error message. Let's modify the sumsq() function to check its arguments and give a more informative error message:

In [2]:
def sumsq(*args):
    out = 0
    for i, x in enumerate(args):
        try:
            out = out + x ** 2
        except TypeError:
            raise ValueError(
                "Argument %s is of type %s but should be numeric" 
                % (i, type(x)))
    return out

print(sumsq(1, 2, "3"))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-5e0ebc683fbd> in <module>()
     10     return out
     11 
---> 12 print(sumsq(1, 2, "3"))

<ipython-input-2-5e0ebc683fbd> in sumsq(*args)
      7             raise ValueError(
      8                 "Argument %s is of type %s but should be numeric"
----> 9                 % (i, type(x)))
     10     return out
     11 

ValueError: Argument 2 is of type <class 'str'> but should be numeric

We still get an error and a traceback, but now the error message is more explicit. Notice the two new ideas in this example:

  • Exceptions can be trapped so that alternative code is run when an exception occurs.

  • Exceptions can be raised by user-written code

Instead of raising a new Exception with our own message, let's try to fix the problem and keep going:

In [3]:
def sumsq(*args):
    out = 0
    for i, x in enumerate(args):
        try:
            out = out + x ** 2
        except TypeError:
            out = out + float(x) ** 2
    return out

print(sumsq(1, 2, "3"))
14.0

As usual in this tutorial, the example is contrived, but it illustrates the point that the Exception mechanism can be used to handle inputs flexibly.

Here is another example. Matplotlib needs to handle several different ways of specifying parameters such as colors, plot aspect ratio, etc. A color might be specified by a string abbreviation of a name, by a string representation of a number from 0-1 for a grey scale, or by an RGB (red, green, blue) sequence. Suppose we want a function that takes any one of these and returns the corresponding RGB tuple (in which each of the three elements is in the 0-1 range).

In [4]:
def to_rgb(color):
    """
    Accept any of several color specifications, and return RGB.
    """
    # Use a dictionary so we can look up the RGB tuples
    # corresponding to a set of single-letter color abbreviations.
    # This could be expanded to include any set of color names.
    colordict = dict(r=(1, 0, 0), g=(0, 1, 0), b=(0, 0, 1), 
                     w=(1, 1, 1), k=(0, 0, 0))
    
    # First, check for the case of a grey value as a string:
    try:
        color + '' #  test: is it a string? 
                    # (Does string concatenation work, or fail?)
        # The line below will be executed only if the test above passed.
        grey = float(color)
    except (TypeError, ValueError):  # not a string, or float() fails
        pass
    else:
        # It looks like a string representation of a grey value.
        if grey <= 1 and grey >= 0:
            return (grey,) * 3  # r, g, b all the same
        else:
            raise ValueError("Argument " + str(color) +
                             " appears to be a grey value" +
                             " but it is not in the 0-1 range.")
    
    # Is it a string in our color dictionary?
    try:
        return colordict[color]
    except KeyError:
        pass
    
    # If we got this far, the input argument is not a grey or
    # a string in our color dictionary, so see if it is a valid
    # RGB tuple:
    try:
        if len(color) != 3:
            raise TypeError
        for v in color:
            v + 1  # test: is it numeric?
                   # if not, this will raise a TypeError
    except TypeError:            
        raise ValueError("Argument " + str(color) + 
                         " is not a string grey value," +
                         " known color abbreviation," +
                         " or RGB tuple.")
    
    # If we got this far, we have 3 numbers, so check the ranges:    
    for v in color:
        if not (v <= 1 and v >= 0):
            raise ValueError("Argument " + str(color) +
                             " has values outside the 0-1 range.")
    return color  # (The input argument is a valid RGB,
                  #  so return it as-is.)
    


# Test by uncommenting the following calls, one at a time:

print(to_rgb("0.5"))  #  grey, e.g., (x,x,x) 
#print(to_rgb(0.5))  #  not a string, so not accepted

#print(to_rgb("b"))   # It's blue in colordict.
#print(to_rgb("c"))   # Not in colordict.

#print(to_rgb((0.2, 0.4, 0.6)))   # Already RGB, so OK.
#print(to_rgb((3, 4, 5)))  # 3 numbers, but not in 0-1 range
(0.5, 0.5, 0.5)

This example illustrates the use of exception handling for flexible argument processing via "Duck typing"; instead of directly checking the type of a variable, we check to see whether it behaves ("quacks") like a given type.

It also illustrates how trying to make things easy for the user of a function, by accepting any of a variety of inputs and figuring out internally how to deal with what the user has provided (including providing helpful error messages), adds a lot of complexity to the code. It's always a tradeoff.

For more about Exceptions, see http://docs.python.org/2/tutorial/errors.html.