At this stage of the lecture, you probably have encountered a couple of error messages when executing your code. Error messages are not nice, but they are frequent (no matter your programming skills) and simply belong to the programming workflow. In this chapter you will learn to recognize the different type of errors, and you will learn how to deal with them.
Copyright notice: parts of this chapter are taken from the official python tutorial
Syntax errors are perhaps the most common kind of complaint you get while you are still learning the language:
t = range(12
t : range(12)
The parser repeats the offending line and displays a little ‘arrow’ pointing at the earliest point in the line where the error was detected. The error is caused by (or at least detected at) the token preceding the arrow. File name and line number are printed so you know where to look in case the input came from a script.
Tip: avoiding syntax errors
If you chose to work with a good IDE, it will let you know about syntax errors way before you even execute the code (this is called a linter). But sometimes it is still difficul to find out what's wrong in a line. The syntax errors which are hardest to track down are missing parentheses:
# bad coding: where's the parenthesis missing?
very_useful_formula = ((10 + 2)**3 - (4 - 5)**2)*10/(5/6) + (6/5)**2)
Here, it helps to separate your code into simpler, multiple-lines statements:
# better coding
numerator = (10 + 2)**3 - (4 - 5)**2
numerator *= 10 # do you know this one?
denominator = (5/6) + (6/5)**2
useful_number = numerator / denominator
Even if a statement or expression is syntactically correct, it may cause an error when an attempt is made to execute it. Errors detected during execution are called exceptions and are not unconditionally fatal: you will soon learn how to handle them. Most exceptions are not handled by programs, however, and result in error messages as shown here:
10 * (1/0)
4 + spam*3
'2' + 2
The last line of the error message indicates what happened. Exceptions come in different types, and the type is printed as part of the message: the types in the example are ZeroDivisionError, NameError and TypeError. The string printed as the exception type is the name of the built-in exception that occurred.
The rest of the line provides detail based on the type of exception and what caused it.
The preceding part of the error message shows the context where the exception happened, in the form of a stack traceback. In general it contains a stack traceback listing source lines; however, it will not display lines read from the interactive interpreter.
When writing programs you will sometimes need to raise exceptions yourself. This can be done with the raise statement:
def useful_addition(a, b):
if a > 12 or b > 12:
raise ValueError('Adding numbers larger than 12 is not possible')
return a + b
A ValueError is often used by programmers to tell the user that they are trying to use non-valid arguments in a function:
useful_addition(4, 22)
Here, I had to take two decisions: which exception should I raise, and which message should I send to the function's caller. I could have taken another, much less informative path:
def bad_addition(a, b):
if a > 12 or b > 12:
# Not recommended
raise RuntimeError('An error ocurred.')
return a + b
It is your job to raise more helpful exceptions than that. The built-in exceptions page lists the built-in exceptions and their meanings. Scroll through the list of possible standard errors: you will see that many of them have meaningful, informative names stating what the error is about. You should learn to use some of them for your own purposes.
The type of exception you might be tempted to use most often are ValueError or TypeError. For example, as a well intentioned programmer you might want to be nice to the user and do the following:
def an_addition(a, b):
# Check input
if type(a) == str or type(b) == str:
raise TypeError('We can only add numbers, not strings!')
if type(a) != type(b):
raise TypeError('We can only add numbers of the same type!')
# OK, go
return a + b
While it is sometimes useful to check the input of a function, this kind of boilerplate code is considered bad practice in python. The reason is that, very often, the error messages sent by python itself are informative enough. See the following examples
def simple_addition(a, b):
return a + b
simple_addition('1', 2)
Here the message is informative enough: so why should we bother adding our own message on top of that? As a rule of thumb: raise your own exception when you think that the default error message is not informative enough.
A good example comes from last week's assignment, where we had to download SRTM files from a server. Two situations could lead to a bad error message:
HTTPError
s are not very informative. In both cases it is recommended to send a better message than that. Here is my implementation the srtm_zone
function I wrote for the assignment:
def srtm_zone(lon, lat):
"""Returns the code of the SRTM zone corresponding to the given location.
Parameters
----------
lon : float
The longitude
lat : float
The latitude
Returns
-------
The SRTM zone (e.g. '02_10')
"""
# Check input
if abs(lat) > 60 or abs(lon) > 180:
raise ValueError('The given coordinates ({}, {}) '.format(lon, lat) +
'do not fit to the available data range [60S;60N].')
[code deleted]
This informs the user in an informative way that the provided input is not valid. For the "ocean" case the problem is not trivial: how to know beforehand if a tile exists or not? For this task we will use another trick, described below.
If you expect parts of a program to raise exceptions in some cases, it might be useful to handle them and avoid the program to stop or to send a cryptic error message. Look at the following example, which asks the user for input until a valid integer has been entered, but allows the user to interrupt the program (using Control-C or whatever the operating system supports):
while True:
try:
x = int(input("Please enter a number: "))
break
except ValueError:
print("Oops! That was no valid number. Try again...")
The try statement works as follows.
A try statement may have more than one except clause, to specify handlers for different exceptions:
try:
a = b + c
except ValueError:
print('Ha!)
except NameError:
print('Ho?)
An except clause may name multiple exceptions as a parenthesized tuple, for example:
except (RuntimeError, TypeError, NameError):
print('All these are ok, we pass...')
But when will we need to catch errors? As explained earlier, it might be useful to replace a bad error message with a more informative one. Here is my implementation of the download_from_server
function I used for the assignment:
from urllib.request import urlretrieve, HTTPError
def download_from_server(zone):
"""Downloads an SRTM file for the given zone.
If the file is already available, skips.
Parameters
----------
zone : str
The file identifier
"""
server_url = 'http://droppr.org/srtm/v4.1/6_5x5_TIFs/'
file_name = 'srtm_{}.zip'.format(zone)
if os.path.exists(file_name):
print('File already available! Skipping...')
return
# Download the file
try:
print('Downloading {}...'.format(file_name))
urlretrieve(server_url+file_name, file_name)
print('Done!')
except HTTPError:
print('The file does not seem to be available on the server. '
'This might be due to the fact that no topography is '
'available at this location (e.g. oceans), or that '
'there was a connection problem. In this case, check your '
'internet connection and try again.')
Note: connection errors are tricky to handle and this will not work in all the cases. This function will need some tuning for more robust solutions, but we leave it as is for now.
Warning messages are typically issued in situations where it is useful to alert the user of some condition in a program, where that condition (normally) doesn’t warrant raising an exception and terminating the program. For example, a library will issue a warning when a program uses an obsolete module. Numpy issues warnings when mathematical computations lead to non-finite results.
Warnings are useful because they do not terminate the program:
import numpy as np
a = np.arange(10)
a / a # issues a warning
This is a nice feature because this invalid division at one location does not imply that the rest of the computations are useless. NaN
s (Not A Number, to be explained in the next lesson) are an indicator for missing data, and most scientific libraries can deal with them.
Depending on your use case, you might want to disable warnings or turn them into errors:
It is possible to temporarily disable warnings with the following syntax:
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
b = a / a
b
In a similar way, you can turn warnings into exceptions:
import warnings
with warnings.catch_warnings():
warnings.simplefilter("error")
b = a / a
The with statement in the examples above is called a context manager. As the name indicates, it serves to temporarily change the context of the code block that follows. In this case, it defines a part of the code where the warnings are silenced or errored. We will get back to context managers later in the lecture.
You can also disable warnings for an entire script or interpreter session simply by filtering them without using the context manager:
warnings.simplefilter("ignore")
This is not recommended, as it might hide important and unexpected warnings.
warnings.filterwarnings gives more control to the filter in order to suppress predefined warnings only:
np.array([2.**100])**100
with warnings.catch_warnings():
warnings.filterwarnings("ignore", "invalid value encountered in true_divide", RuntimeWarning)
# Divide by zero is ignored
b = a / a
# But overflow is not
np.array([2.**100])**100
try ... except
statements help to catch expected errors and do something about it. This is particularly useful in software which need to run whatever happens. Back to the table of contents, or jump to the next chapter.