10 Exception handling
This section deals with what to do in Python to make sure (un)expected errors during the runtime are handled in such a way that the program is not closing down in an unexpected way.
We will not deal with Syntax Errors here.
10.1 Exceptions
The BaseException class is the base class for the built-in exceptions (see docs and tutorials) and all the built-in exception are inherited from this class. For writing our own exceptions however, we should use Exception as the base.
In order to introduce the concept let us show some of the most common exceptions
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
for number in numberlist:
inverse = 1.0 / number
print(f"Check: {inverse} * {number} = {inverse * number}")Check: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[1], line 4 1 numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4] 3 for number in numberlist: ----> 4 inverse = 1.0 / number 5 print(f"Check: {inverse} * {number} = {inverse * number}") ZeroDivisionError: float division by zero
We can see that an ZeroDivisionError is raised for the third entry in the list of numberlist. What happened above is not a graceful exit as our little program just stopped at the point of exception and the last action of the interpreter was to print the traceback. Importantly, the other elements in the list are never looked at.
We can handle exceptions with the try-except construction to actually make sure that we react appropriately.
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
1try:
for number in numberlist:
inverse = 1.0 / number
print(f"Check: {inverse} * {number} = {inverse * number}")
2except ZeroDivisionError:
3 print("Warning: zero does not have an inverse!")- 1
-
The try clause starts with
tryand ends withexceptand is executed normal - 2
-
The exception clause starts at
except, we can specify for which exception to look for. - 3
-
The code in the context of
exceptis only accessed if an exception occurs.
Check: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
Warning: zero does not have an inverse!
From the output we can see that the exception occurred and was handled properly. Nevertheless, the rest of the elements in the list are still not handled. To make sure this is the case we need to move our try/except into the loop.
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
1for number in numberlist:
2 try:
inverse = 1.0 / number
print(f"Check: {inverse} * {number} = {inverse * number}")
3 except ZeroDivisionError:
print("Warning: zero does not have an inverse!")- 1
-
Context of the
forloop - 2
-
Context of the
tryclause - 3
-
Context of the
exceptclause
Check: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
Warning: zero does not have an inverse!
Check: 1.0 * 1 = 1.0
Check: 0.5 * 2 = 1.0
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[3], line 5 3 for number in numberlist: # <1> 4 try: # <2> ----> 5 inverse = 1.0 / number # <2> 6 print(f"Check: {inverse} * {number} = {inverse * number}")# <2> 7 except ZeroDivisionError: # <3> TypeError: unsupported operand type(s) for /: 'float' and 'str'
Now everything works as expected and we see our second error TypeError happen for "a" in the list. We have multiple options as this point how to deal with more than one exception.
If we use multiple except we can interpret them similar to if, elif, and else statements, where we jump into one part as soon as we match.
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
for number in numberlist:
try:
inverse = 1.0 / number
print(f"Check: {inverse} * {number} = {inverse * number}")
1 except ZeroDivisionError:
print("Warning: zero does not have an inverse!")
2 except TypeError:
print("Error: The inverse is only defined for a number!")- 1
-
Context of the
ZeroDivisionError - 2
-
Context of the
TypeError
Check: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
Warning: zero does not have an inverse!
Check: 1.0 * 1 = 1.0
Check: 0.5 * 2 = 1.0
Error: The inverse is only defined for a number!
Check: 4.0 * 0.25 = 1.0
Similar to when we inherited for multiple classes, we can combine exceptions by putting them into a set (with ( and )).
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
for number in numberlist:
try:
inverse = 1.0 / number
print(f"Check: {inverse} * {number} = {inverse * number}")
1 except (ZeroDivisionError, TypeError):
print("Error: I can not compute the inverse!") - 1
-
Handling multiple errors in one
exceptclause.
Check: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
Error: I can not compute the inverse!
Check: 1.0 * 1 = 1.0
Check: 0.5 * 2 = 1.0
Error: I can not compute the inverse!
Check: 4.0 * 0.25 = 1.0
The construct has one further extension. After all of the except blocks we can place an else as well finally at the very end. The else block is called if no exception occurred and the finally is always called.
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
for number in numberlist:
try:
inverse = 1.0 / number
1 except BaseException as be:
2 print(f"Error: I can not compute the inverse of {number}!")
print(f"Message is: {be}")
3 else:
print(f"Check: {inverse} * {number} = {inverse * number}")
4 finally:
print("Let us work on the next element")- 1
- Inheritance allows you to match for all child error classes
- 2
- If we give the exception a name we can access it during the error handling.
- 3
- This block is only called if no exception occurred
- 4
- This block is always called and normally used to clean up
Check: -0.5 * -2 = 1.0
Let us work on the next element
Check: -1.0 * -1 = 1.0
Let us work on the next element
Error: I can not compute the inverse of 0!
Message is: float division by zero
Let us work on the next element
Check: 1.0 * 1 = 1.0
Let us work on the next element
Check: 0.5 * 2 = 1.0
Let us work on the next element
Error: I can not compute the inverse of a!
Message is: unsupported operand type(s) for /: 'float' and 'str'
Let us work on the next element
Check: 4.0 * 0.25 = 1.0
Let us work on the next element
Of course you can also work with exception and raise them yourself, see docs for all the details.
To raise an exception we use
raise ValueError("No value given.")--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[7], line 1 ----> 1 raise ValueError("No value given.") ValueError: No value given.
We can chain exceptions in case something happens during the handling of an exception
try:
open("missing.txt")
except OSError as e:
raise RuntimeError("Oh good, I can not handle this at the moment.") from e--------------------------------------------------------------------------- FileNotFoundError Traceback (most recent call last) Cell In[8], line 2 1 try: ----> 2 open("missing.txt") 3 except OSError as e: File ~/work/MECH-M-DUAL-1-SWD/MECH-M-DUAL-1-SWD/.venv/lib/python3.12/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs) 318 raise ValueError( 319 f"IPython won't let you open fd={file} by default " 320 "as it is likely to crash IPython. If you know what you are doing, " 321 "you can use builtins' open." 322 ) --> 324 return io_open(file, *args, **kwargs) FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt' The above exception was the direct cause of the following exception: RuntimeError Traceback (most recent call last) Cell In[8], line 4 2 open("missing.txt") 3 except OSError as e: ----> 4 raise RuntimeError("Oh good, I can not handle this at the moment.") from e RuntimeError: Oh good, I can not handle this at the moment.
To deactivate the chaining we use from None.
Alternatively, you can enrich the error with a note:
try:
raise ValueError("No value given.")
except ValueError as e:
e.add_note("Oh good, I can not handle this at the moment.")
raise--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[9], line 2 1 try: ----> 2 raise ValueError("No value given.") 3 except ValueError as e: 4 e.add_note("Oh good, I can not handle this at the moment.") ValueError: No value given. Oh good, I can not handle this at the moment.
10.2 with statements
For certain objects it is clear that some clean-up is required. The prime example is accessing a file:
path = "exception.qmd"
1file = open(path, "r")
try:
print("The first line of this file is:")
2 print(file.readline())
finally:
3 file.close()- 1
- Open the file
- 2
- Read from the file
- 3
- We always need to close the file again.
The first line of this file is:
# Exception handling {#sec-error-exceptions}
The short and easier to read version is
path = "exception.qmd"
1with open(path, "r") as file:
print("The first line of this file is:")
print(file.readline())- 1
- Automatically closes the file as soon as the context is closed.
The first line of this file is:
# Exception handling {#sec-error-exceptions}
This is not a full replacement of a try-except block.
path = "exception1.qmd"
try:
with open(path, "r") as file:
print("The first line of this file is:")
print(file.readline())
except OSError as os:
print(f"Something happened: {os}")Something happened: [Errno 2] No such file or directory: 'exception1.qmd'
If you write your own class you can include the necessary functions for the with statement as follows, Example from GeeksForGeeks:
class MessageWriter(object):
def __init__(self, file_name):
self.file_name = file_name
def __enter__(self):
self.file = open(self.file_name, "w")
return self.file
def __exit__(self, *args):
self.file.close()
# using with statement with MessageWriter
with MessageWriter("my_file.txt") as xfile:
xfile.write("hello world")It is also possible to collect exceptions and raise them in a group.
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
errors = []
for i, number in enumerate(numberlist):
try:
inverse = 1.0 / number
except BaseException as be:
be.add_note(f"Happened for entry {i}")
errors.append(be)
if len(errors) > 0:
raise ExceptionGroup("The following exceptions occurred:", errors) + Exception Group Traceback (most recent call last):
| File "/home/runner/work/MECH-M-DUAL-1-SWD/MECH-M-DUAL-1-SWD/.venv/lib/python3.12/site-packages/IPython/core/interactiveshell.py", line 3577, in run_code
| exec(code_obj, self.user_global_ns, self.user_ns)
| File "/tmp/ipykernel_2724/285673109.py", line 11, in <module>
| raise ExceptionGroup("The following exceptions occurred:", errors)
| ExceptionGroup: The following exceptions occurred: (2 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "/tmp/ipykernel_2724/285673109.py", line 6, in <module>
| inverse = 1.0 / number
| ~~~~^~~~~~~~
| ZeroDivisionError: float division by zero
| Happened for entry 2
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "/tmp/ipykernel_2724/285673109.py", line 6, in <module>
| inverse = 1.0 / number
| ~~~~^~~~~~~~
| TypeError: unsupported operand type(s) for /: 'float' and 'str'
| Happened for entry 5
+------------------------------------
10.3 Assertions
There is an additional feature that is a good way to make sure that conditions are met before working on something. For this assert can be used. It tests if a condition is true and if this is not the case an AssertionError is raised.
import numbers
numberlist = [-2, -1, 0, 1, 2, "a", 1 / 4]
for number in numberlist:
try:
assert isinstance(number, numbers.Number)
assert number != 0
inverse = 1.0 / number
except AssertionError:
print(f"Error: I can not compute the inverse of {number}!")
else:
print(f"Check: {inverse} * {number} = {inverse * number}")Check: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
Error: I can not compute the inverse of 0!
Check: 1.0 * 1 = 1.0
Check: 0.5 * 2 = 1.0
Error: I can not compute the inverse of a!
Check: 4.0 * 0.25 = 1.0
This feature is usually used during development. Therefore, there is an option -O to deactivate it during the run of a python program.
This is often done, due to the fact that each of these assert statements takes some time to be processed.
pdm run python -O test_assert.pyCheck: -0.5 * -2 = 1.0
Check: -1.0 * -1 = 1.0
Traceback (most recent call last):
File "/home/runner/work/MECH-M-DUAL-1-SWD/MECH-M-DUAL-1-SWD/rs/../_assets/errorhandling/test_assert.py", line 8, in <module>
inverse = 1.0 / number
~~~~^~~~~~~~
ZeroDivisionError: float division by zero