Beginner

What are errors good for ?


When we say that errors are a feature that let you know that something that you might not have expected occured, there are a few key words in this sentence :

  • Errors are a feature not a bug, they are helpful in understanding what went wrong and what part of the code require attention.
  • Let you know : errors are loud and detailed not to be confusing but to be specific.
  • You might not have expected because expecting an error at a specific spot allow you to handle it.

Let's look at an example of error handling.

ToC

In [1]:
value_N1 = 0
value_N2 = 20

try:
    growth = value_N2 / value_N1

except ZeroDivisionError:
    growth = None
    print('The N+2 value is 0, therefore growth cannot be calculated.')
The N+2 value is 0, therefore growth cannot be calculated.

The try block contains code that is expected to throw an error, the except block catches the error and deals with it.


Before getting into why and how to handle errors properly, there are two important concepts to go over.

Beginner

It is Easier to Ask for Forgiveness than Permission.


When working with python, there is this concept that it is "easier to ask for forgiveness than permission". This is opposed to another programming concept named "look before you leap".


Let's look at an example of "look before you leap" code. Let's assume we have values for a year N2 and want to know the growth from N1. For some reason, some values for N1 are missing, maybe they did not exist a year prior. With the "LBYL" approach, we would do that :

ToC

In [2]:
# Two lists of values.
lst_N1 = [10, 25, 0, 6, 24, 15, 0]
lst_N2 = [8, 28, 15, 6, 21, 18, 7]

# Putting them by pair with the built-in zip function.
pairs = list(zip(lst_N1, lst_N2))

# Iterating through the pairs and building a list of calculated growth.
growth = []
for pair in pairs:
    # First we test if the division is going to be possible ...
    if pair[0] != 0:
        # ... Then and only then do we compute the growth.
        growth.append(pair[1] / pair[0])
    else:
        # If the division is not possible we just append None.
        growth.append(None)

print(growth)
[0.8, 1.12, None, 1.0, 0.875, 1.2, None]

This approach is called "look before you leap" because before every division we test if the second value is 0 or not, when we are sure the division can be called safely, then we divide.


This method is not wrong, but is not the most efficient way of handeling things. In most cases, when attempting something, we expect it to succed. In this example most values are strictly positive integers but we have to check for the rare occasion where it is in fact 0.

Let's do the same operation with the "EAFP" method.

In [3]:
# Two lists of values.
lst_N1 = [10, 25, 0, 6, 24, 15, 0]
lst_N2 = [8, 28, 15, 6, 21, 18, 7]

# Putting them by pair with the built-in zip function.
pairs = list(zip(lst_N1, lst_N2))

# Iterating through the pairs and building a list of calculated growth.
growth = []
for pair in pairs:
    try:
        # The division is tried no matter what.
        growth.append(pair[1] / pair[0])
    except ZeroDivisionError:
        # If the division throws an error, we append None instead.
        growth.append(None)

print(growth)
[0.8, 1.12, None, 1.0, 0.875, 1.2, None]

The result is the same except a lot less tests (if statements) are performed in this code.

Beginner

Duck typing.


Duck typing is another important concept related to error handling. The gist of it is : "if it quacks like a duck, and fly like a duck then assume it is a duck".

The underlying concept here is that if an object can do the thing you want it to do then you do not have to worry about its type.

Let's see an example.

ToC

In [4]:
l1 = [1, 2, 3, 4]

print(sum(l1))
10
In [5]:
s1 = {1, 2, 3, 4}

print(sum(s1))
10
In [6]:
import pandas as pd

serie_1 = pd.Series([1, 2, 3, 4])

print(sum(serie_1))
10

Here we can see that the built-in sum function works with any object that behaves like a collection of numbers, even on a pandas.Series that is not a buily-in type.

"If it quack like a duck, treat it like a duck".


This notion is related to the "EAFP" method and error handling because, just like before, we don't lose time with checking if we can do something, rather we try to do it and then handle the case where it does not work.

Beginner

Error handling.


With all that information, it is easier to understand why error handling is important :

  • Handling specific situation that you know yoy can work arround.
  • Optimize your code with less checking and more error handling.
  • Write more pythonic code, with duck typing and "EAFP" methods.
  • Raise your own errors when necessary.

Now let's look at how to handle errors, with an example.

ToC

In [7]:
try:
    file = open('file.txt', 'r')
    # Do stuff.
    file.close()
except FileNotFoundError:
    print('The file was not found but the program does not need to be stopped here.')

print('The program continues here.')
The program continues here.

This example, with the same structure as our first example, can be extended like so :

In [8]:
try:
    file = open('file.txt', 'r')
except FileNotFoundError:
    print('The file was not found but the program does not need to be stopped here.')
else:
    str_text = file.read()
    print(str_text)

    file.close()
finally:
    print('This is printed no matter what.')
Line 1
Line 2
Line 3


This is printed no matter what.

In this example the main blocks are :

  • try : contains everything that might throw an error that we want to catch.
  • except : handle a specific error. An except block can handle multiple errors, and a try statement can have multiple except statements.
  • else : contains everything that must be executed after the try statement. It is possible to write everything inside the try block but using an else statement is cleaner.
  • finally : contains everything that needs to be executed no matter what.

Note : in that example an error can still occur in the else statement which would prevent the close method to be called. The closed method itself cannot be moved as is in the finally block because it would be executed even if the file does not exists.

This example can be made a little more complex.

In [9]:
# we try to access the file without checking if it is possible or not.
try:
    file = open('file.txt', 'r')
# If the file does not exist, we print a message and do nothing else.
except FileNotFoundError:
    print('The file was not found but the program does not need to be stopped here.')
else:
    # If the file does exist then we try to read it.
    try:
        str_text = file.read()
        print(str_text)
    # If anything happen during the reading of the file, we close it.
    finally:
        file.close()
finally:
    print('This is printed no matter what.')
Line 1
Line 2
Line 3


This is printed no matter what.

A last, a more complex example just to show what is possible to do.

In [10]:
# Code that can throw an error.
try:
    pass
# A very specific except clause.
except ZeroDivisionError:
    pass
# A slightly more generic except clause.
except (AttributeError, NameError) as e:
    pass
# A very generic except clause.
except Exception as e:
    pass
# Code that is executed if no exception is caught.
else:
    pass
# Code that is executed no matter what.
finally:
    pass

When using multiple except block, the more specific should go first because otherwise an error can be caught in a wider except statement.


Also using a bare except: statement is not recommended not only because you might catch unwanted errors but also because it catchs KeyboardInterrupt errors, and prevent the user to stop a program caught in an infinite loop, for example.