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.
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 |
|
Index is out of bounds, for example, |
|
A reference to an undefined dictionary key |
|
A name not found, for example, an undefined variable |
|
Errors in the |
|
Incompatible data value, for example, when using |
|
I/O operation fails, for example, "file not found" |
|
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.
Let's look at the basic principles on how to use exceptions by raising them with raise
and catching them with try
statements.
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.
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.
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.
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.
18.188.178.181