5. Exceptions
We will now turn our attention to exceptions.

5.1. script [exceptions_01]
The first script illustrates the need to handle exceptions.
We intentionally trigger an error to see what happens (exceptions-01.py):
Line 4 gives us:
- the exception type: ZeroDivisionError;
- the associated error message: division by zero. It is in English. This is something you might want to change.
A key rule is that we must do everything possible to avoid exceptions generated by the Python interpreter. We need to handle errors ourselves.
The syntax for exception handling is as follows:
try:
actions that may raise an exception
except (Ex1, Ex2…) as ex:
actions to handle the [ex] exception
except (Ex11, Ex12…) as ex:
actions to handle the exception [ex]
finally:
actions that are always executed, regardless of whether an exception occurs
In the try block, execution of the actions stops as soon as an exception (error) occurs. In this case, execution continues with the actions of one of the except clauses:
- line 3: if the exception [ex] that occurred is of a type belonging to the tuple (Ex1, Ex2…) or derived from one of them, then the actions on line 4 are executed;
- line 5: if the exception was not caught by line 3, and another [except] clause exists, then the same process occurs. Etc…;
- there can be as many [except] clauses as necessary to handle the different types of exceptions that may occur within the [try] block;
- if the exception has not been handled by any of the [except] clauses, then it will propagate back to the calling code. If the calling code is itself within a try/except structure, the exception is handled again; otherwise, it continues to propagate up the chain of called methods. Ultimately, it reaches the Python interpreter. The interpreter then terminates the running program and displays an error message of the type shown in the previous example. The rule is therefore that the main program must catch all exceptions that may propagate from called methods;
- line 7: the [finally] clause is always executed, whether an exception occurred (following the except) or not (following the try). This is true even if an exception occurred and was not caught. In this case, the [finally] clause will be executed before the exception propagates back to the calling code;
An exception carries information about the error that occurred. This information can be obtained using the following syntax:
except MyException as exception:
[exception] is the exception that occurred. [exception.args] represents the tuple of the exception’s parameters.
To throw an exception, use the syntax
raise MyException(param1, param2…)
where MyException is most often a class derived from the BaseException class. The parameters passed to the class constructor will be available to the except clause of exception-handling structures using the syntax [ex.args] if [ex] is the exception caught by the [except] clause.
These concepts are illustrated by the following script.
5.2. script [exceptions_02]
The following script explicitly handles errors:
# exception handling
test = 0
# We trigger an error and handle it
x = 2
try:
x = 4 / 0
except ZeroDivisionError as error:
# error is the caught exception
print(f"Attempt #{attempt}: {error}")
# the value of x has not changed
print(f"x={x}")
# let's try again
trial += 1
try:
x = 4 / 0
except BaseException as error:
# We catch the most general exception
# error is the caught exception
print(f"test #{test}: {error}")
# we can catch different types of exceptions
# execution stops at the first [except] block capable of handling the exception
trial += 1
try:
x = 4 / 0
except ValueError as error:
# this exception does not occur here
print(f"Attempt #{attempt}: {error}")
except BaseException as error:
# we catch the most general exception
print(f"test #{test}: (Exception) {error}")
except ZeroDivisionError as error:
# catch a specific type
print(f"test #{test}: (ZeroDivisionError) {error}")
# Let's try again, changing the order
trial += 1
try:
x = 4 / 0
except ValueError as error:
# This exception does not occur here
print(f"Attempt #{attempt}: {error}")
except ZeroDivisionError as error:
# we catch a specific type
print(f"test #{test}: (ZeroDivisionError) {error}")
except BaseException as error:
# catching the most general exception
print(f"test #{test}: (Exception) {error}")
# An except clause without arguments
trial += 1
try:
x = 4 / 0
except:
# we are not interested in the nature of the exception
print(f"Test #{test}: There was a problem")
# another type of exception
trial += 1
try:
# x cannot be converted to an integer
x = int("x")
except ValueError as error:
# error is the caught exception
print(f"test #{test}: {error}")
# An exception carries information in a tuple accessible to the program
trial += 1
try:
x = int("x")
except ValueError as error:
# error is the caught exception
print(f"Attempt #{attempt}: {error}, arguments={error.args}")
# exceptions can be raised
trial += 1
try:
raise ValueError("param1", "param2", "param3")
except ValueError as error:
# error is the caught exception
print(f"Test #{test}: {error}, arguments={error.args}")
# You can create your own exceptions
# they must be derived from the [BaseException] class
class MyError(BaseException):
pass
# we raise the MyError exception
try += 1
try:
raise MyError("info1", "info2", "info3")
except MyError as error:
# error is the caught exception
print(f"Test #{test}: {error}, arguments={error.args}")
# we raise the MyError exception with an error message
trial += 1
try:
raise MyError("my error message")
except MyError as error:
# error is the caught exception
print(f"test #{test}: {error.args[0]}")
# the finally clause is always executed
# regardless of whether an exception occurs
trial += 1
x = None
try:
x = 1
except:
# exception
print(f"test #{test}: exception")
finally:
# executed in all cases
print(f"test #{test}: finally x={x}")
trial += 1
x = None
try:
x = 2 / 0
except:
# exception
print(f"test #{test}: exception")
finally:
# executed in all cases
print(f"test #{test}: finally x={x}")
# You don't have to include an [except] clause
trial += 1
try:
# we cause an error
x = 4 / 0
finally:
# executed in all cases
print(f"test #{test}: finally x={x}")
Notes:
- lines 4–12: handle a division by zero;
- line 8: we catch the exact exception that occurs and display it;
- line 12: because of the exception that occurred, x did not receive a value on line 7 and therefore did not change its value;
- lines 14–21: we do the same thing but catch a higher-level exception of type BaseException. Since the ZeroDivisionError exception is a subclass of BaseException, the except clause will catch it;
- lines 23–36: we use multiple except clauses to handle multiple exception types. Only one except clause will be executed, or none at all if the exception does not match any except clause;
- lines 38–50: We do the same thing, changing the order of the [except] clauses to demonstrate their role;
- lines 52–58: The `except` clause may have no arguments. In this case, it catches all exceptions;
- lines 60–67: introduce the ValueError exception;
- lines 69–75: we retrieve the information carried by the exception;
- lines 77–83: introduce how to raise an exception;
- lines 86–106: illustrate the use of a custom exception class, MyError. The MyError class simply inherits from the base class BaseException. It adds nothing to its base class. However, it can now be explicitly named in except clauses;
- lines 108–130: illustrate the use of the finally clause;
- lines 132–139: these lines show that the [except] clause is not mandatory;
The screen output is as follows:
5.3. script [exceptions_03]
This new script illustrates how exceptions are propagated up the chain of calling functions:
# a custom exception
class MyError(BaseException):
pass
# three functions
def f1(x: int) -> int:
# We don't handle exceptions—they are automatically propagated
return f2(x)
def f2(y: int) -> int:
# We don't handle exceptions—they are automatically propagated
return f3(y)
def f3(z: int) -> int:
if (z % 2) == 0:
# if z is even, raise an exception
raise MyError("exception in f3")
else:
return 2 * z
# ---------- main
# Exceptions propagate up the chain of called methods
# until a method catches it. Here, it will be main
try:
print(f1(4))
except MyError as error:
print(f"type: {type(error)}, arguments: {error.args}")
# three other functions that enrich the exceptions it raises
def f4(x: int) -> int:
try:
return f5(x)
except MyError as error:
# we enrich the exception and then re-raise it
raise MyError("exception in f4", error)
def f5(y: int) -> int:
try:
return f6(y)
except MyError as error:
# we add more details to the exception and then re-raise it
raise MyError("exception in f5", error)
def f6(z: int) -> int:
if (z % 2) == 0:
# raise an exception if z is even
raise MyError("exception in f6")
else:
return 2 * z
# ---------- main
try:
print(f4(4))
except MyError as error:
# display the exception
print(f"type: {type(error)}, arguments: {error.args}")
# we can trace the exception stack
err = error
# display the error message
print(err.args[0])
# Is an exception encapsulated?
while len(err.args) == 2 and isinstance(err.args[1], BaseException):
# change exception
err = err.args[1]
# The first argument is the error message
print(err.args[0])
Notes:
- lines 25–32: in the call main → f1 → f2 → f3 (line 30), the MyError exception raised by f3 will propagate up to main. It will then be handled by the except clause on line 31;
- lines 61–75: in the call main → f4 → f5 → f6 (line 62), the MyError exception raised by f6 will propagate up to main. It will then be handled by the except clause on line 63. This time, as it propagates up the chain of calling functions, the MyError exception that propagates is itself encapsulated within another exception;
- lines 66–75: show how to trace the exception stack;
- line 71: the function [isinstance(instance, Class)] returns True if the object [instance] is of type [Class] or a subclass. Here, we have used the highest-level exception [BaseException], which ensures we catch all exceptions;
The screen output is as follows: