Exceptions

Gernerally speaking, in Python an Exception describes an occured error. Thus, exception handling deals with reacting to errors and deal with them once they occured. The difference between an exception and a malfunction or bug is the expectability of that error. You handle errors that you would also expect with a specific probability. On the other side, a bug is often a logical or syntactical error in your code. Both can not be handled by exception handling.
A sytactical error will prevent the interpreter from running the code at all, which also includes the error handling. A logical error is not a real error, it's just something that doesn't make any sense. The real source of a logical error is usually sitting in front of the computer. This makes the handling of logical errors the most complicated part in testing and debugging your code.
Exceptions are instances of the Exception class or an inheriting one. Thus, an Exception can also be isntantiated with a real Exception occuring. All these classes have one thing in common: they accept at least one argument: the error message.


In [ ]:
# instantiate 2 exceptions
exc1 = Exception()
exc2 = Exception('Hier lief was schief.')
print('Type: ',type(exc1),'Str: ', str(exc1))
print('Type: ', type(exc2),'Str: ', str(exc2))

There is one more key statement in Python that can only be used for Exceptions: the **raise** statement.
This will trigger the Exception. In informatics you will often find the term *'to throw and exception' as well. So, let's raise exc2:


In [ ]:
raise exc2

You could use that to raise your own Exceptions in case the user of your scripts give you wrong input to your functions. Or in case you want to change the sometimes very generic error messages produced by the standard classes.


In [ ]:
def divide_print(a, b):
    if b == 0:
        print('b must not be zero, idiot!')
    return a / b

Print the function


In [ ]:
print(divide_print(5, 0))

This time we were able to print a custom error message, but the default Exception is still raised as the program is not interrupted. One option would be to move the return statement into a else clause. The consequence would be a non-explicit function which does return a result only sometime. Additionally, the code gets quite complext for an easy logic like preventing a division by zero. More complex program logics would turn into non-readable code. The other problem of None-returning functions os the reusability of the function result, as one would have to check the return values for being None everytime.
Therefore it's better to raise your personalized Exception:


In [ ]:
def divide_exc(a, b):
    if b == 0:
        raise Exception('b must not be zero, idiot!')

In [ ]:
print(divide_exc(5,0))

Now the Exception uses the correct Message. Unfortunately it's just an Exception, not a ZeroDivisionError anymore. The most explicit soultion would therefore be to raise an instance of the most suitable child class of Exception using a custom error message. This is easily possible as all of these classes accept an error message as first argument. In case there is no suitable child class, we could easily write our own (which is usually not necessary). Before doing so, let's raise the different error types:

error types


In [ ]:
import nonexistingpackage

In [ ]:
5 / 0

In [ ]:
min(5, 4, 'straßenbahn')

In [ ]:
open()

In [ ]:
foo

In [ ]:
# this will only raise an Error on Windows machines
open(5)

In [ ]:
fs = open('afile.txt', 'w')
fs.superpower

In [ ]:
open('afile.txt', 'k')

Inspecting the error types above, either the ZeroDivisionError or or ValueError would have been the most appropiate ones.


In [ ]:
def divide_exc(a,b):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise ValueError('a and b must be numerical. a: {0} b: {1}'.format(type(a), type(b)))
    if b == 0:
        raise ZeroDivisionError('b must not be 0, idiot')
    return a / b

In [ ]:
divide_exc(5, 0)

In [ ]:
divide_exc(5, None)

Exception handling

Up to this point we can raise Errors, inherit from existing, write custom error messages or built a new Error class. But how should they be handeled in a project?
It is possible to instruct the python interpreter not to just kill itself on case an error occurs in a specified block of code. Then the interpreter will just interrupt this block and run another block of code for handling the Exception. This handling block will be ignored in case no error occured. It is possible to write a general handling block for all types of errors or to specifiy multiple blocks handling only a specified Error Type. This is especially useful in case only a very specific type of error is expected and all other Exceptions would be unexpected.


In [ ]:
try:
    divide_exc(5, 0)
except:
    print('Just like something happend.\n')

try:
    divide_exc(5,None)
except ValueError:
    print('This was a ValueError.\n')

try:
    divide_exc(5,0)
except ZeroDivisionError:
    print('definitely a ZeroDivisionError.\n')
    
try:
    divide_exc(5,None)
except ValueError as e:
    print('ALERT! ALERT! ALERT!\nAn critical ValueError occured.\nIt said something like:\n%s.\n-----\n' % str(e))

Instead of just decorating the error message any handling is possible. One could exit the application, produce a graphical error message, log the exception into a file, continue using default values and so on.
You can even have different except blocks:


In [ ]:
def run_divide(a, b):
    try:
        result = 'a / b = {}'.format(divide_exc(a, b))
    except ValueError as e:
        result = 'Error. wrong input.'
    except ZeroDivisionError:
        result = 'a / 1 = {} (b must not be 0, using b=1...)'.format(divide_exc(a, 1))
    
    return result

print('1.: ', run_divide(9, 5))
print('2.: ', run_divide(9, 0))
print('3.: ', run_divide(9, 'five'))

Any try except construct can also include an else clause and be finished by a finally.


In [ ]:
def run(a, b):
    try:
        print(divide_exc(a,b))
    except ValueError:
        print('Wrong Input')
    except ZeroDivisionError:
        print('b must not be 0')
    else:
        print('No error occured')
    finally:
        print('---------------\n')
        
run(5,6)
run(3, 'five')
run(4, 0)