Errors, Exceptions and Warnings#
At this stage of the lecture, you probably have encountered several (many?) 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 types 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#
Syntax errors are perhaps the most common kind of complaint you get while you are still learning the language:
t = range(12
Cell In[1], line 1
t = range(12
^
_IncompleteInputError: incomplete input
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 choose 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). However, sometimes it is still difficult to find out what is 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)
Cell In[2], line 2
very_useful_formula = ((10 + 2)**3 - (4 - 5)**2)*10/(5/6) + (6/5)**2)
^
SyntaxError: unmatched ')'
Here, it helps to separate your code into simpler, multi-line 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
Exceptions#
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)
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[4], line 1
----> 1 10 * (1/0)
ZeroDivisionError: division by zero
4 + spam*3
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[5], line 1
----> 1 4 + spam*3
NameError: name 'spam' is not defined
'2' + 2
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 1
----> 1 '2' + 2
TypeError: can only concatenate str (not "int") to str
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 above examples 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.
Raising Exceptions#
The raise statement#
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)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[8], line 1
----> 1 useful_addition(4, 22)
Cell In[7], line 3, in useful_addition(a, b)
1 def useful_addition(a, b):
2 if a > 12 or b > 12:
----> 3 raise ValueError('Adding numbers larger than 12 is not possible')
4 return a + b
ValueError: Adding numbers larger than 12 is not possible
Raise the correct exception#
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.
When to raise an exception?#
The types of exception you might be tempted to use most often are ValueError and TypeError. For example, a well-intentioned programmer might want to be nice to the user and write 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)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[12], line 1
----> 1 simple_addition('1', 2)
Cell In[11], line 2, in simple_addition(a, b)
1 def simple_addition(a, b):
----> 2 return a + b
TypeError: can only concatenate str (not "int") to str
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.
An example is our previous assignment downloading data from the GeoSphere Austria Data Hub, where you were asked to include a check that ensures that the start and end times provided by the user are formatted correctly. Without this additional check, an exception will be raised when the data download fails but without information on why it failed.
%run download_geosphere_tawes_without_format_check.py TL 11320 20250801T00:00 20251001T00:00
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_without_format_check.py:59, in download_tawes(param, station_id, start, end)
58 else:
---> 59 print(f'Data download error {data.status_code}')
60 except:
NameError: name 'data' is not defined
During handling of the above exception, another exception occurred:
RuntimeError Traceback (most recent call last)
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_without_format_check.py:212
208 print('download_geosphere_tawes parameter station_id ' +
209 'start (YYYY-MM-DDThh:mm) end (YYYY-MM-DDThh:mm)')
210 sys.exit()
--> 212 main(args, output_dir=output_dir, create_plot=create_plot)
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_without_format_check.py:172, in main(api_args, output_dir, create_plot)
154 ''' download current or historical TAWES data from the GeoSphere Austria Data Hub
155 and print data to screen or save to file
156
(...) 164 create_plot: bool
165 '''
167 # # check that start and end date/time are formatted correctly
168 # if len(api_args) == 4:
169 # check_date_time(api_args[2:])
170
171 # download TAWES data from the Data Hub
--> 172 data = download_tawes(*api_args)
174 # print current data point to the terminal
175 if len(api_args) == 2:
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_without_format_check.py:61, in download_tawes(param, station_id, start, end)
59 print(f'Data download error {data.status_code}')
60 except:
---> 61 raise RuntimeError('Data download failed')
63 return response.json()
RuntimeError: Data download failed
On the other hand, when checking the date format, we can provide the user with a meaningful error message.
%run download_geosphere_tawes_with_format_check.py TL 11320 20250801T00:00 20251001T00:00
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_with_format_check.py:212
208 print('download_geosphere_tawes parameter station_id ' +
209 'start (YYYY-MM-DDThh:mm) end (YYYY-MM-DDThh:mm)')
210 sys.exit()
--> 212 main(args, output_dir=output_dir, create_plot=create_plot)
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_with_format_check.py:169, in main(api_args, output_dir, create_plot)
167 # check that start and end date/time are formatted correctly
168 if len(api_args) == 4:
--> 169 check_date_time(api_args[2:])
171 # download TAWES data from the Data Hub
172 data = download_tawes(*api_args)
File ~/teaching/UIBK_ScientificProgramming/UIBK_ProgrammingMS_Winter2025/scientific_programming/book/unit_08/download_geosphere_tawes_with_format_check.py:149, in check_date_time(datestrings)
146 for datestring in datestrings:
147 if ((datestring[4] != '-') or (datestring[7] != '-') or
148 (datestring[10] != ' ') or (datestring[13] != ':')):
--> 149 raise ValueError('Start and end dates must conform to the format YYYY-mm-dd HH:MM')
ValueError: Start and end dates must conform to the format YYYY-mm-dd HH:MM
Handling exceptions#
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.
First, the try clause (the statement(s) between the try and except keywords) is executed.
If no exception occurs, the except clause is skipped and execution of the try statement is finished.
If an exception occurs during execution of the try clause, the rest of the clause is skipped. Then if its type matches the exception named after the except keyword, the except clause is executed, and then execution continues after the try statement.
If an exception occurs which does not match the exception named in the except clause, it is passed on to outer try statements; if no handler is found, it is an unhandled exception and execution stops with a message as shown above.
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...')
Warnings#
Warning messages are typically issued in situations where it is useful to alert the user of some condition in a program, which (normally) does not 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
/tmp/ipykernel_375616/2968769965.py:4: RuntimeWarning: invalid value encountered in divide
a / a # issues a warning
array([nan, 1., 1., 1., 1., 1., 1., 1., 1., 1.])
This is a nice feature because this invalid division at one location does not imply that the rest of the computations are useless. NaNs 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.
Silencing warnings#
It is possible to temporarily disable warnings with the following syntax:
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
b = a / a
b
array([nan, 1., 1., 1., 1., 1., 1., 1., 1., 1.])
Warnings as exceptions#
In a similar way, you can turn warnings into exceptions:
import warnings
with warnings.catch_warnings():
warnings.simplefilter("error")
b = a / a
---------------------------------------------------------------------------
RuntimeWarning Traceback (most recent call last)
Cell In[17], line 5
3 with warnings.catch_warnings():
4 warnings.simplefilter("error")
----> 5 b = a / a
RuntimeWarning: invalid value encountered in divide
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.
Filter warnings by type or message#
warnings.filterwarnings gives more control to the filter in order to suppress predefined warnings only:
np.array([2.**100])**100
/tmp/ipykernel_375616/3776166511.py:1: RuntimeWarning: overflow encountered in power
np.array([2.**100])**100
array([inf])
with warnings.catch_warnings():
warnings.filterwarnings("ignore", "invalid value encountered", RuntimeWarning)
# Divide by zero is ignored
b = a / a
# But overflow is not
np.array([2.**100])**100
/tmp/ipykernel_375616/3074648172.py:6: RuntimeWarning: overflow encountered in power
np.array([2.**100])**100
Take home points#
Exceptions are not something to be afraid of: they are helping us to find problems in our code.
There are different types of exceptions, which is useful to help us understand what is causing the problem. Often, an additional error message is printed too, further explaining the problem.
You can raise exceptions yourself in your code.
The
try ... exceptstatements help to catch expected errors and do something about them. This is particularly useful in software which needs to run no matter what happens.