⇦ Back

Here is some code that produces an error:

ls = ['List Item 0', 'List Item 1', 'List Item 2']
print(ls[3])
## Error in py_call_impl(callable, dots$args, dots$keywords): IndexError: list index out of range
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

Under normal circumstances the script will stop when an error like this is encountered, but this isn’t the only option. This page will look at how to do error handling to customise what happens when an error is triggered.

1 Using Exceptions

Errors can be handled using exceptions, also known as try/excepts because those are the words you use:

ls = ['List Item 0', 'List Item 1', 'List Item 2']
try:
    # Try to run this code
    print(ls[3])
except:
    # Run this code if we encounter an exception
    pass

In the above snippet we tried to run the line print(ls[3]) but this raised an exception (because there are only 3 elements in the list and, because Python is a zero-index language, indexing element “3” tries to return the 4th item in the list). However, as opposed to causing the script to stop, this error instead caused the exception clause to run. In this example, the exception clause contained the pass statement, which does nothing. As a result, the script continued to run and reached the end without failing! We have ‘avoided’ the error caused by the problematic print(ls[3]) line.

1.1 Don’t Use ‘Bare Excepts’

While the above example is a good first introduction to exception handling, it contains an example of bad practice: it uses the except statement on its own. You should instead explicitly say which type of errors it should catch:

ls = ['List Item 0', 'List Item 1', 'List Item 2']
try:
    # Try to run this code
    print(ls[3])
except IndexError:
    # Run this code if we encounter an IndexError exception
    pass

This way, we have built a script that can handle a very specific type of error: an Index Error. We are expecting that this type of error might occur and are preparing the script to behave in an expected fashion should it indeed be encountered. However, if something unexpected were to go wrong and an error were to exist that we were not expecting, the script will still fail. This is good: it’s better to have the script failing if there’s a genuine mistake rather than having it running to completion and hiding the error from you.

1.2 A Realistic Example

Often, you will have a set of data and will look through it trying to find a certain value. If that value does not exist, you might decide that you still want your script to continue running:

values = [1.8, 1.9, 2.0]
try:
    # Try to run this code
    solution = next(idx for idx, value in enumerate(values) if value > 3)
except StopIteration:
    # Run this code if we encounter a StopIteration exception
    pass

This code will search for a value larger than 3 in the list of values, fail to find one, trigger a Stop Iteration error (because it has reached the end of the list) and execute the pass statement before continuing on.

1.3 Else/Finally Statements

These can be used when you don’t know exactly what type of error to expect:

x = 5
try:
    result = 1 / x
except ZeroDivisionError:
    print('A ZeroDivisionError occurred')
else:
    print('No error occurred')
finally:
    print('This will print regardless of whether an error occurred or not')
## No error occurred
## This will print regardless of whether an error occurred or not

1.4 Useful Error Messages

Using f-strings can be a good way to communicate to the user what it was that caused the script to fail:

dct = {'Key 0': 0}
try:
    key = 'Key 1'
    print(dct[key])
except KeyError:
    print(f'The key "{key}" does not exist in the dictionary')
## The key "Key 1" does not exist in the dictionary

2 Raising an Error Manually

An error can be raised on purpose by using the (surprise, surprise) raise statement:

raise ValueError
## Error in py_call_impl(callable, dots$args, dots$keywords): ValueError: 
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

An explanation for why you raised the error can be included as a parameter for the error statement:

raise ValueError('The reason for this error being raised')
## Error in py_call_impl(callable, dots$args, dots$keywords): ValueError: The reason for this error being raised
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

As already mentioned, f-strings can be used to make the error messages more informative:

val = 5
if val == 5:
    raise ValueError(f'This error was raised because val = {val}')
## Error in py_call_impl(callable, dots$args, dots$keywords): ValueError: This error was raised because val = 5
## 
## Detailed traceback:
##   File "<string>", line 2, in <module>

2.1 If/Else Statements

When using if/else statements, the relevant error to handle is the Value Error:

condition = False
if condition:
    pass
else:
    raise ValueError('An If/Else error occurred')
## Error in py_call_impl(callable, dots$args, dots$keywords): ValueError: An If/Else error occurred
## 
## Detailed traceback:
##   File "<string>", line 4, in <module>

This can be used as a crude way of creating a custom error type:

CustomError = True
if CustomError:
    raise ValueError('My custom error has been triggered')
## Error in py_call_impl(callable, dots$args, dots$keywords): ValueError: My custom error has been triggered
## 
## Detailed traceback:
##   File "<string>", line 2, in <module>

However, the correct way to create custom error types is to define them as a new class:

3 Using Custom Error Types

Creating you own error type can help you in being specific about what exactly went wrong in your code. You can create error types to cover all the sorts of errors you expect to be generated when your code is being used by other people, and set up responses to handle each type correctly. Setting up a new error type is as easy as defining an empty class that takes an Exception parameter:

class MyCustomErrorType(Exception):
    pass

This can then be invoked using raise:

raise MyCustomErrorType
## Error in py_call_impl(callable, dots$args, dots$keywords): MyCustomErrorType: 
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

A text explanation can be included as with the other error types:

raise MyCustomErrorType('The reason for this error being raised')
## Error in py_call_impl(callable, dots$args, dots$keywords): MyCustomErrorType: The reason for this error being raised
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

If you have the text “__main__.” at the start of the error message (which isn’t the case in these examples, but you might see it elsewhere) you can remove it by overwriting the default __module__ value:

class MyCustomErrorType(Exception):
    """Raise for my specific kind of exception."""

    # Remover "__main__" from the beginning of the exception
    __module__ = Exception.__module__

And, as before, remember that f-strings can make the error messages more useful.

4 Using Assertions

If you make the effort to set things up correctly you’ll be able to use errors to your advantage. If your code produces something that is wrong it’s better to have it fail and tell you why it was wrong as opposed to it soldiering on and producing an incorrect result that you don’t realise is wrong! One way to achieve this is by using assertions which, essentially, are an error type of their own: if an assertion fails it triggers an Assertion Error which halts the script.

4.1 Built-In Assertions

Python comes with the assert statement which evaluates something and triggers an error if the evaluation produces a False result:

assert False, 'Oh no! This assertion failed!'
## Error in py_call_impl(callable, dots$args, dots$keywords): AssertionError: Oh no! This assertion failed!
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

Here’s an example where the assertion passes:

assert 1 == 1, 'Oh no! This assertion failed!'

Here again we have an example with an f-string as the error message:

a = 1
b = 2
assert a == b, f'The assertion failed because {a} != {b}'
## Error in py_call_impl(callable, dots$args, dots$keywords): AssertionError: The assertion failed because 1 != 2
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>

4.2 Hamcrest Assertions

Hamcrest is a package that can be installed with python3.10 -m pip install PyHamcrest where python3.10 is the version of Python you have installed. It contains a wider array of options that can be used for assertions, starting with the assert_that() function which can work with two parameters (one evaluation and one error message) as follows:

from hamcrest import assert_that

assert_that(False, 'Oh no! This assertion failed!')
## Error in py_call_impl(callable, dots$args, dots$keywords): AssertionError: Oh no! This assertion failed!
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 62, in assert_that
##     _assert_bool(assertion=cast(bool, actual), reason=cast(str, matcher))
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 80, in _assert_bool
##     raise AssertionError(reason)

With three parameters (two evaluations and one error message) a number of ‘matchers’ are defined in the package which can be used to carry out the test for the assertion, for example equal_to():

from hamcrest import equal_to

assert_that(True, equal_to(False), 'Oh no! This assertion failed!')
## Error in py_call_impl(callable, dots$args, dots$keywords): AssertionError: Oh no! This assertion failed!
## Expected: <False>
##      but: was <True>
## 
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 58, in assert_that
##     _assert_match(actual=actual, matcher=matcher, reason=reason)
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 73, in _assert_match
##     raise AssertionError(description)

The is_() matcher is ‘syntactic sugar’: it does absolutely nothing but it helps with readability:

from hamcrest import is_

assert_that(True, is_(False), 'Oh no! This assertion failed!')
## Error in py_call_impl(callable, dots$args, dots$keywords): AssertionError: Oh no! This assertion failed!
## Expected: <False>
##      but: was <True>
## 
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 58, in assert_that
##     _assert_match(actual=actual, matcher=matcher, reason=reason)
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 73, in _assert_match
##     raise AssertionError(description)
assert_that(True, is_(equal_to(False)), 'Oh no! This assertion failed!')
## Error in py_call_impl(callable, dots$args, dots$keywords): AssertionError: Oh no! This assertion failed!
## Expected: <False>
##      but: was <True>
## 
## 
## Detailed traceback:
##   File "<string>", line 1, in <module>
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 58, in assert_that
##     _assert_match(actual=actual, matcher=matcher, reason=reason)
##   File "/home/rowan/.local/lib/python3.10/site-packages/hamcrest/core/assert_that.py", line 73, in _assert_match
##     raise AssertionError(description)

The in statement also works:

option = 'First'
accepted_options = ['First', 'Second']
assert_that(option in accepted_options, 'The option is not one of the provided options')

⇦ Back