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 ... except statements help to catch expected errors and do something about them. This is particularly useful in software which needs to run no matter what happens.