Exceptions

The language C does not provide any mechanism for handling errors. That is a pity because in C, we must always handle error propagation by writing functions that returns 0 in general, and a value different from 0 in case of an error. The code becomes horrible.

We also have nasty setjmp and longjmp which are dangerous!. Many languages, like C++, Python, Javascript, Java, C# etc. offer exceptions to handle errors. They work like setjmp and longjmp, but they take care about the memory.

3/0
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

Cell In[1], line 1
----> 1 3/0


ZeroDivisionError: division by zero
import logging

y = 0
try:
    x = 3/y
except ZeroDivisionError:
    logging.error("You are crazy to try a division by zero!")
ERROR:root:You are crazy to try a division by zero!

This is particularly useful when the error occurs deep in the program.

def division(x, y):
    return x/y

def f(x, y):
    return division(x, y) + 1

try:
    print(f(3, 0))
except ZeroDivisionError:
    print("You are crazy to try a division by zero!")
You are crazy to try a division by zero!

Syntax of try-except-else-finally

try:
    print("write here the code what may cause an exception")
except ZeroDivisionError:
    print("write here what to do in case of a division by zero")
except AssertionError:
    print("write here what to do in case of an assert condition was false")
else:
    print("write here some code to execute when no error occurred")
finally:
    print("write here code which is always executed (error or not)")
write here the code what may cause an exception
write here some code to execute when no error occurred
write here code which is always executed (error or not)
def f():
    try:
        x = 3/0
    except ZeroDivisionError:
        print("write here what to do in case of a division by zero")
        return
    except AssertionError:
        print("write here what to do in case of an assert condition was false")
    else:
        print("write here some code to execute when no error occurred")
    finally:
        print("write here code which is always executed (error or not)")

f()
write here what to do in case of a division by zero

Hierarchy of exceptions

from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("Exception", "BaseException"),
           ("KeyboardInterrupt", "BaseException"),
           ("SystemExit", "BaseException"),
           ("ZeroDivisionError", "Exception"),
           ("AssertionError", "Exception"),
           ("ValueError", "Exception"),
           ])
dot

svg

try:
    3/0
except BaseException:
    print("All base exceptions are handled")
NEVER catch a BaseException. The program should STOP in that case

All exceptions are BaseException. Normally, you do not catch KeyboardInterrupt, SystemExit because you prevent your program from stopping if the user wants so!

The following code is bad:

try:
    print("my program")
except BaseException:
    print("NEVER catch a BaseException. The program should STOP in that case")
try:
    print("my program")
except KeyboardInterrupt:
    print("NEVER catch a BaseException. The program should STOP in that case")

However, you can catch exceptions that inherits from Exception

User-defined exceptions

You can define your own type of exceptions.

class DenominatorZeroError(Exception):
    pass

def f(x,y):
    if y == 0:
        raise DenominatorZeroError
    return x/y

try:
    d = f(3, 0)
except DenominatorZeroError:
    print("Denominator should be non-zero!")

Denominator should be non-zero!
class DenominatorZeroError(Exception):
    pass

def g(x):
    return DenominatorZeroError if x == 0 else ValueError

def f(x,y):
    if y == 0:
        raise g(0)
    return x/y

try:
    d = f(3, 0)
except DenominatorZeroError:
    print("Denominator should be non-zero!")

Denominator should be non-zero!
class OddNumberOfVertexError(Exception):
    def __init__(self, nbVertices):
        super().__init__(f"should give a graph with an even number of vertices: {nbVertices} given")

def computePerfectMatching(G):
    if G["nbVertices"] % 2 != 0:
        raise OddNumberOfVertexError(G["nbVertices"])


try:
    computePerfectMatching({"nbVertices": 3})
except OddNumberOfVertexError as e:
    print(e)


should give a graph with an even number of vertices: 3 given

Ressource management

Bad practice

In Python, it is highly not-recommended to write:

for line in open('example.txt'): # bad
    ...
---------------------------------------------------------------------------

FileNotFoundError                         Traceback (most recent call last)

Cell In[14], line 1
----> 1 for line in open('example.txt'): # bad
      2     ...


File /usr/local/lib/python3.9/site-packages/IPython/core/interactiveshell.py:310, in _modified_open(file, *args, **kwargs)
    303 if file in {0, 1, 2}:
    304     raise ValueError(
    305         f"IPython won't let you open fd={file} by default "
    306         "as it is likely to crash IPython. If you know what you are doing, "
    307         "you can use builtins' open."
    308     )
--> 310 return io_open(file, *args, **kwargs)


FileNotFoundError: [Errno 2] No such file or directory: 'example.txt'

The issue is that... we do not close the file! Also do not write (even many websites say it):

f = open("example.txt")
print(file.read()) 
file.close()

In case of an error in the treatment, the file may not be closed.

Solution

Instead we should a construction which has the following form:

file = EXPR
file.__enter__()
try:
    BLOCK
finally:
    file.__exit__()

This is cumbersome. So thanks to https://peps.python.org/pep-0343/ we have a with-as construction:

with EXPR as file:
    BLOCK
with open('example.txt') as file:
    content = file.read()
---------------------------------------------------------------------------

FileNotFoundError                         Traceback (most recent call last)

Cell In[1], line 1
----> 1 with open('example.txt') as file:
      2     content = file.read()


File /usr/local/lib/python3.9/site-packages/IPython/core/interactiveshell.py:310, in _modified_open(file, *args, **kwargs)
    303 if file in {0, 1, 2}:
    304     raise ValueError(
    305         f"IPython won't let you open fd={file} by default "
    306         "as it is likely to crash IPython. If you know what you are doing, "
    307         "you can use builtins' open."
    308     )
--> 310 return io_open(file, *args, **kwargs)


FileNotFoundError: [Errno 2] No such file or directory: 'example.txt'

We can also use several ressources as follows.

with open('input.txt') as fInput, open("output.txt", "w") as fOutput:
    for line in fInput:
        fOutput.write(line)