Chapter 10. Error Handling

In this chapter, we will cover errors, exceptions, and how to find and fix them. Handling exceptions is an important part of writing reliable and usable code. We will introduce the basic built-in exceptions and show how to use and treat exceptions. We'll introduce debugging and show you how to use the built-in Python debugger.

What are exceptions?

One error programmers (even experienced ones) find is when code has incorrect syntax, meaning that the code instructions are not correctly formatted.

Consider an example of Syntax error:

>>> for i in range(10)
  File “<stdin>”, line 1
    for i in range(10)
                      ^
SyntaxError: invalid syntax

The error occurs because of a missing colon at the end of the for declaration. This is an example of an exception being raised. In the case of SyntaxError, it tells the programmer that the code has incorrect syntax and also prints the line where the error occurred, with an arrow pointing to where in that line the problem is.

Exceptions in Python are derived (inherited) from a base class called Exception. Python comes with a number of built-in exceptions. Some common exception types are listed in Table 10.1, (for full list of built-in exceptions refer to [38]).

Here are two common examples of exceptions. As you might expect, ZeroDivisionError is raised when you try to divide by zero.

def f(x):
    return 1/x

>>> f(2.5)
0.4 
>>> f(0)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "exception_tests.py", line 3, in f
    return 1/x
ZeroDivisionError: integer division or modulo by zero

Exception

Description

IndexError

Index is out of bounds, for example, v[10] when v only has 5 elements

KeyError

A reference to an undefined dictionary key

NameError

A name not found, for example, an undefined variable

LinAlgError

Errors in the linalg module, for example, when solving a system with a singular matrix

ValueError

Incompatible data value, for example, when using dot with incompatible arrays

IOError

I/O operation fails, for example, "file not found"

ImportError

A module or name is not found on import

Table10.1: Some frequently used built-in exceptions and their meaning

A division with zero raises ZeroDivisionError and prints out the file, line, and function name where the error occurred.

As we have seen before, arrays can only contain elements of the same data type. If you try to assign a value of an incompatible type, a ValueError is raised. An example, of a value error:

>>> a = arange(8.0) 
>>> a 
array([ 0., 1., 2., 3., 4., 5., 6., 7.]) 
>>> a[3] = 'string'
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
ValueError: could not convert string to float: string

Here, ValueError is raised because the array contains floats and an element cannot be assigned a string value.

Basic principles

Let's look at the basic principles on how to use exceptions by raising them with raise and catching them with try statements.

Raising exceptions

Creating an error is referred to as raising an exception. You saw some examples of exceptions in the previous section. You can also define your own exceptions, of a predefined type or type-less. Raising an exception is done with the command like this:

raise Exception("Something went wrong")

It might be tempting to print out error messages when something goes wrong, for example, like this:

print("The algorithm did not converge.")

This is not recommended for a number of reasons. Firstly, printouts are easy to miss, especially if the message is buried in many other messages being printed to your console. Secondly, and more importantly, it renders your code unusable by other code. The calling code will have no way of knowing that an error occurred and therefore have no way of taking care of it.

For these reasons, it is always better to raise an exception instead. Exceptions should always contain a descriptive message, for example:

raise Exception("The algorithm did not converge.")

This message will stand out clearly for the user. It also gives the opportunity for the calling code to know that an error occurred, and to possibly find a remedy.

Here is a typical example of checking the input inside a function to make sure it is usable before continuing. For an example, a simple check for negative values and the correct data type ensures the intended input of a function to compute factorials:

def factorial(n):
  if not (n >=0 and isinstance(n,(int,int32,int64))):
    raise ValueError("A positive integer is expected")
    ...

The user of the function will immediately know what the error is, if an incorrect input is given, and it is the user's responsibility to handle the exception. Note the use of the exception name when raising a predefined exception type, in this case ValueError followed by the message. By specifying the type of the exception, the calling code can decide to handle errors differently depending on what type of error is raised.

Summing up, it is always better to raise exceptions than to print error messages.

Catching exceptions

Dealing with an exception is referred to as catching an exception. Checking for exceptions is done with the try and except commands.

An exception stops the program execution flow and looks for the closest try enclosing block. If the exception is not caught, the program unit is left and it continues searching for the next enclosing try block in a program unit higher up in the calling stack. If no block is found and the exception is not handled, execution stops entirely; the standard traceback information is displayed.

Let's look at an example for the try statement:

try:
    <some code that might raise an exception>
except ValueError:
    print("Oops, a ValueError occurred")

In this case, if the code inside the try block raises an error of type ValueError, the exception will be caught and the message in the except block printed. If no exception occurs inside the try block, the except block is skipped entirely and execution continues.

The except statement can catch multiple exceptions. This is done by simply grouping them in a tuple, like this:

except (RuntimeError, ValueError, IOError):

The try block can also have multiple except statements. This makes it possible to handle exceptions differently depending on the type. Let's see an example of multiple exception types:

try:
    f = open('data.txt', 'r')
    data = f.readline()
    value = float(data)
except OSError as oe:
    print("{}:  {}".format(oe.strerror, oe.filename))
except ValueError:
    print("Could not convert data to float.")

Here an OSError will be caught if, for example, the file does not exist; and a ValueError will be caught if, for example, the data in the first line of the file is not compatible with the float data type.

In this example we assigned the OSError to a variable oe by the keyword as. This allows to access more details when handling this exception. Here we printed the error string oe.strerror and the name of the related file oe.filename. Each error type can have its own set of variables depending on the type. If the file does not exist, in the preceding example, the message will be:

I/O error(2): No such file or directory

On the other hand, if the file exists but you don’t have permission to open it, the message will be:

I/O error(13): Permission denied

This is a useful way to format the output when catching exceptions.

The try  -  except combination can be extended with optional else and finally blocks. An example of using else can be seen in section Testing the bisection algorithm of Chapter 13, Testing. Combining try with finally gives a useful construction when cleanup work needs to happen at the end:

An example for making sure a file is closed properly:

try:
    f = open('data.txt', 'r')
    # some function that does something with the file
    process_file_data(f) 
except: 
    ... 
finally:
    f.close()

This will make sure that the file is closed at the end no matter what exceptions are thrown while processing the file data. Exceptions that are not handled inside the try statement are saved and raised after the finally block. This combination is used in the with statement; see section Context Managers - the with statement.

User-defined exceptions

Besides the built-in Python exceptions, it is also possible to define your own exceptions. Such user-defined exceptions should inherit from the Exception base class. This can be useful when you define your own classes like the polynomial class in section Polynomials of Chapter 14, Comprehensive Examples.

Take a look at this small example of a simple user-defined exception:

class MyError(Exception):
    def __init__(self, expr):
        self.expr = expr
    def __str__(self):
        return str(self.expr)

try:
   x = random.rand()
   if x < 0.5:
      raise MyError(x)
except MyError as e:
   print("Random number too small", e.expr)
else:
   print(x)

A random number is generated. If the number is below 0.5, an exception is thrown and a message that the value is too small is printed. If no exception is raised, the number is printed.

In this example, you also saw a case of using else in a try statement. The block under else will be executed if no exception occurs.

It is recommended that you define your exceptions with names that end in Error, like the naming of the standard built-in exceptions.

Context managers - the with statement

There is a very useful construction in Python for simplifying exception handling when working with contexts, such as files or databases. The statement encapsulates the try ... finally structure in one simple command. Here is an example of using with to read a file:

with open('data.txt', 'r') as f:
    process_file_data(f)

This will try to open the file, run the specified operations on the file (for example, reading), and close the file. If anything goes wrong during the execution of process_file_data, the file is closed properly and then the exception is raised. This is equivalent to:

f = open('data.txt', 'r')
try: 
    # some function that does something with the file 
    process_file_data(f) 
except:
    ... 
finally:
    f.close()

We will use this option in section File handling of Chapter 12, Input and Output, when reading and writing files.

The preceding file reading example is an example of using context managers. Context managers are Python objects with two special methods, _ _enter_ _ and _ _exit_ _. Any object of a class that implements these two methods can be used as a context manager. In this example, the file object f  is a context manager as there are f._ _enter_ _ and f._ _exit_ _ methods.

The _ _enter_ _ method should implement the initialization instructions, for example, opening a file or a database connection. If this method has a return statement, the returned object is accessed using the as construct. Otherwise, the as keyword is omitted. The _ _exit_ _ method contains the cleanup instructions, for example, closing a file or committing transactions and closing a database connection. For more explanations and an example of a self-written context manager, see the section Timing with a context manager of Chapter 13, Testing.

There are NumPy functions that can be used as context managers. For example, the load function supports context manager for some file formats. NumPy's function errstate can be used as a context manager to specify floating-point error handling behavior within a block of code.

Here is an example of working with errstate and a context manager:

import numpy as np      # note, sqrt in NumPy and SciPy 
                        # behave differently in that example
with errstate(invalid='ignore'):
    print(np.sqrt(-1)) # prints 'nan'

with errstate(invalid='warn'):
    print(np.sqrt(-1)) # prints 'nan' and 
                   # 'RuntimeWarning: invalid value encountered in sqrt'

with errstate(invalid='raise'):
    print(np.sqrt(-1)) # prints nothing and raises FloatingPointError

Refer section Infinite and Not a Number of Chapter 2, Variables and Basic Types, for more details on this example and section Timing with a context manager of Chapter 13, Testing for another example.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.178.181