Skip to content

5. Exceptions

We will now turn our attention to exceptions.

Image

5.1. script [exceptions_01]

The first script illustrates the need to handle exceptions.


# on provoque volontairement une erreur
x = 4 / 0

We intentionally trigger an error to see what happens (exceptions-01.py):

1
2
3
4
5
6
7
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py", line 2, in <module>
    x = 4 / 0
ZeroDivisionError: division by zero

Process finished with exit code 1

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 susceptibles de lancer une exception
except (Ex1, Ex2…) as ex:
    actions de gestion de l'exception [ex]
except (Ex11, Ex12…) as ex:
    actions de gestion de l'exception [ex]
finally:
   actions toujours exécutées qu'il y ait exception ou non

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
essai = 0

#  cause an error and manage it
x = 2
try:
    x = 4 / 0
except ZeroDivisionError as erreur:
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur}")
#  the value of x has not changed
print(f"x={x}")

#  here we go again
essai += 1
try:
    x = 4 / 0
except BaseException as erreur:
    #  we intercept the most general exception
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur}")

#  different types of exceptions can be intercepted
#  execution stops on the 1st [except] able to handle the exception
essai += 1
try:
    x = 4 / 0
except ValueError as erreur:
    #  this exception does not occur here
    print(f"essai n° {essai} : {erreur}")
except BaseException as erreur:
    #  we intercept the most general exception
    print(f"essai n° {essai} : (Exception) {erreur}")
except ZeroDivisionError as erreur:
    #  we intercept a specific type
    print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")

#  start again, changing the order 
essai += 1
try:
    x = 4 / 0
except ValueError as erreur:
    #  this exception does not occur here
    print(f"essai n° {essai} : {erreur}")
except ZeroDivisionError as erreur:
    #  we intercept a specific type
    print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
except BaseException as erreur:
    #  we intercept the most general exception
    print(f"essai n° {essai} : (Exception) {erreur}")

#  an except clause with no arguments
essai += 1
try:
    x = 4 / 0
except:
    #  we're not interested in the nature of the exception
    print(f"essai n° {essai} : il y a eu un problème")

#  another type of exception
essai += 1
try:
    #  x cannot be converted to an integer
    x = int("x")
except ValueError as erreur:
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur}")

#  an exception transports information in a tuple accessible to the program
essai += 1
try:
    x = int("x")
except ValueError as erreur:
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")

#  exceptions can be thrown
essai += 1
try:
    raise ValueError("param1", "param2", "param3")
except ValueError as erreur:
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")


#  you can create your own exceptions
#  they must derive from class [BaseException]
class MyError(BaseException):
    pass


#  throw MyError exception
essai += 1
try:
    raise MyError("info1", "info2", "info3")
except MyError as erreur:
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")

#  throw MyError exception with error msg
essai += 1
try:
    raise MyError("mon msg d'erreur")
except MyError as erreur:
    #  error is the exception intercepted
    print(f"essai n° {essai} : {erreur.args[0]}")

#  the finally clause is always executed
#  whether or not there is an exception
essai += 1
x = None
try:
    x = 1
except:
    #  exception
    print(f"essai n° {essai} : exception")
finally:
    #  executed in all cases
    print(f"essai n° {essai} : finally x={x}")

essai += 1
x = None
try:
    x = 2 / 0
except:
    #  exception
    print(f"essai n° {essai} : exception")
finally:
    #  executed in all cases
    print(f"essai n° {essai} : finally x={x}")

#  we don't have to include an except clause
essai += 1
try:
    #  we cause an error
    x = 4 / 0
finally:
    #  executed in all cases
    print(f"essai n° {essai} : 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:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py", line 136, in <module>
    x = 4 / 0
ZeroDivisionError: division by zero
essai n° 0 : division by zero
x=2
essai n° 1 : division by zero
essai n° 2 : (Exception) division by zero
essai n° 3 : (ZeroDivisionError) division by zero
essai n° 4 : il y a eu un problème
essai n° 5 : invalid literal for int() with base 10: 'x'
essai n° 6 : invalid literal for int() with base 10: 'x', paramètres=("invalid literal for int() with base 10: 'x'",)
essai n° 7 : ('param1', 'param2', 'param3'), paramètres=('param1', 'param2', 'param3')
essai n° 8 : ('info1', 'info2', 'info3'), paramètres=('info1', 'info2', 'info3')
essai n° 9 : mon msg d'erreur
essai n° 10 : finally x=1
essai n° 11 : exception
essai n° 11 : finally x=None
essai n° 12 : finally x=None

Process finished with exit code 1

5.3. script [exceptions_03]

This new script illustrates how exceptions are propagated up the chain of calling functions:

#  a proprietary exception
class MyError(BaseException):
    pass


#  three functions
def f1(x: int) -> int:
    #  exceptions are not handled - they are automatically raised
    return f2(x)


def f2(y: int) -> int:
    #  exceptions are not handled - they are automatically raised
    return f3(y)


def f3(z: int) -> int:
    if (z % 2) == 0:
        #  if z is even, throw an exception
        raise MyError("exception dans f3")
    else:
        return 2 * z


#  ---------- hand

#  exceptions go up the chain of methods called
#  until a method intercepts it. In this case, it will be main
try:
    print(f1(4))
except MyError as erreur:
    print(f"type : {type(erreur)}, arguments : {erreur.args}")


#  three other functions that enrich the exceptions it tracks
def f4(x: int) -> int:
    try:
        return f5(x)
    except MyError as erreur:
        #  we enrich the exception then relaunch it
        raise MyError("exception dans f4", erreur)


def f5(y: int) -> int:
    try:
        return f6(y)
    except MyError as erreur:
        #  we enrich the exception then relaunch it
        raise MyError("exception dans f5", erreur)


def f6(z: int) -> int:
    if (z % 2) == 0:
        #  throw an exception if z is even
        raise MyError("exception dans f6")
    else:
        return 2 * z


#  ---------- hand
try:
    print(f4(4))
except MyError as erreur:
    #  exception display
    print(f"type : {type(erreur)}, arguments : {erreur.args}")
    #  you can move up the exception stack
    err = erreur
    #  error msg is displayed
    print(err.args[0])
    #  is an exception encapsulated?
    while len(err.args) == 2 and isinstance(err.args[1], BaseException):
        #  exception change
        err = err.args[1]
        #  the 1st argument is the error msg
        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:

1
2
3
4
5
6
7
8
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_03.py
type : <class '__main__.MyError'>, arguments : ('exception dans f3',)
type : <class '__main__.MyError'>, arguments : ('exception dans f4', MyError('exception dans f5', MyError('exception dans f6')))
exception dans f4
exception dans f5
exception dans f6

Process finished with exit code 0