Help support the author by donating or purchasing a copy of the book (not available yet)



Chapter 14 - Error Handling

14.1 - Raising Exceptions

In Python you'll run into many kinds of errors and I'm sure you've ran into plenty so far!

We've seen syntax errors which arise from improper syntax e.g. too many closing brackets. You'll also run into something called exception errors. Exception errors arise whenever syntactically correct Python code results in an error. There are many types of exception errors, to name a few, you'll get an ImportError when you try to import a module that cannot be found, you'll get a ZeroDivisionError when the second operand of the division or modulo operator is zero. If you run into an error that doesn't fall into a specific category of exception, Python will throw a RuntimeError.

Sometimes in our programs we want to raise an exception if a specific condition occurs. This condition might not be one that will crash our program, but we as programmers don't want it occurring.

To raise an exception we use the raise statement. The condition we are raising the exception for may be one that is illogical. For example, we may have a program that changes the speed of a car. It would be illogical for our car to go at a negative speed as there is no such thing.

We would raise the exception as follows:

def decrease_velocity(curr_vel, decrease):
    return curr_vel - decrease

def main():
    car_velocity = 20
    inp = int(input())
    while inp:
        car_velocity = decrease_velocity(car_velocity, inp)
        if car_velocity < 0:
            raise Exception("Car cannot have a negative speed")            
        inp = int(input())
        
if __name__=="__main__":
    main()

Running this with the following inputs would give the following output:

$ py change_vel.py
5
6
7
5
Traceback (most recent call last):
  File "test3.py", line 14, in <module>
    main()
  File "test3.py", line 10, in main
    raise Exception("Car cannot have a negative speed")            
Exception: Car cannot have a negative speed

As you can see we raised an exception with our own, custom error message.

We can even raise specific types of exceptions. A ValueError may be suited to this example and we simply replace Exception with ValueError.

We may also raise exceptions where we expect errors to occur as they normally would. We would raise an exception in this case as to provide a more detailed error message.

But, as a general rule of thumb, an exception is raised when a fundamental assumption of the current code is found to be false.

14.2 - Assertions

If you remember back to the beginning of this book I talked about pre and post conditions. A pre condition is a condition that always holds true prior to the execution of some code. Pre conditions may be used in functions to enforce a contract between a function and its invoker. Let's assume we have some function that requires a list as an argument. We want to assert that the argument passed to the function is a list and not something else like a string.

We implement assertions in python using: assert

def sorter(arr):
    assert(type(arr) == list)
    # Do the sorting

def main():

    arr = [6, 3, 5, 1, 8]
    sorter(arr)

    print("We've made it this far")

    arr = "Hello"
    sorter(arr)
        
if __name__=="__main__":
    main()

running the above code produces the following output:

$ py assert.py
We've made it this far
Traceback (most recent call last):
  File "test3.py", line 16, in <module>
    main()
  File "test3.py", line 13, in main
    sorter(arr)
  File "test3.py", line 2, in sorter
    assert(type(arr) == list)
AssertionError

As you can see our assertion was True when we passed the list so the program continued as normal. When we passed a string to the sorter function, the assertion returned False and our program exited and raised an AssertionError.

14.3 - try & except

In this section I'm going to talk about handling exceptions that we may encounter in our code. In Python, to handle exceptions we use a try and except block. We catch the errors using these constructs.

The way try and except blocks work are not all that different from a if and else statement. Inside the try block we put code that should run as "normal". If an exception is raised in the try block then we catch this exception in the except block. By catching the exception we can handle the error without our program crashing.

Let me give two examples. One where we don't try to catch the exception and one where we do.

filename = input()
with open(filename) as f:
    lines = f.readlines()
    
print(lines)

This would produce the following error:

$ py read_file.py
Traceback (most recent call last):
  File "test3.py", line 1, in <module>
    with open("IDontExist.txt") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'IDontExist.txt'

Our program has crash. What if we wanted to handle this appropriately and allow the user to enter the filename again? We use a try and except block to do this.

filename = input("Enter a file name: ")
while filename:
    try:
        with open(filename) as f:
            print(f.readlines())
            filename = input("Enter a file name: ")
    except FileNotFoundError:
        print("That file doesn't exist in the current directory")
        filename = input("Enter a file name: ")

Now if we run our code:

$ py read_files.py
Enter a file name: test.txt
That file doesn't exist in the current directory
Enter a file name: input.txt
['This is line one.\n', "I'm line two.\n", "And I'm line three"]
Enter a file name: wrongfile.txt
That file doesn't exist in the current directory

We can see, rather than our program crashing, we handle the FileNotFoundError in a correct manor and allow the user to re-enter a filename.

Python has an exception hierarchy. This is shown below:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

We were being quite specific with our exception in the previous example. Assume another error might occur somewhere in our program and we are unsure of what it might be but we still want to allow the user to enter a filename after it occurs. We can have multiple except clauses after the try block.

filename = input("Enter a file name: ")
while filename:
    try:
        with open(filename) as f:
            print(f.readlines())
            filename = input("Enter a file name: ")
    except FileNotFoundError:
        print("That file doesn't exist in the current directory")
        filename = input("Enter a file name: ")
    except:
        print("Unexpected error occured")
        filename = input("Enter a file name: ")

Now, if we run into any other error we have handled it with a more general exception.

We also handle exceptions we explicitly raise in the same way:

inp = int(input())
try:
    if inp < 5:
        raise ValueError
    else:
        print(inp)
except ValueError:
    print("Input shouldn't be less than 5")

14.4 - finally

Usually, if we have a try and except block, we'll try to do something specific that may be prone to errors. In the example from the previous section, that specific something was opening a file. We shouldn't have to ask the user for input again in the try block. We also shouldn't have to ask the user to enter the filename again after we handled the exception in the except block.

Instead, we want some way of cleaning up afterwards. In Python, we do this with the finally statement.

This is demonstrated below:

filename = input("Enter a file name: ")
while filename:
    try:
        with open(filename) as f:
            print(f.readlines())
    except FileNotFoundError:
        print("That file doesn't exist in the current directory")
    except:
        print("Unexpected error occured")
    finally:
        filename = input("Enter a file name: ")

The finally block is always executed even if no exceptions were raised in the try block. Although our code is prone to errors from the users input, we can handle that correctly thus making our code more robust.

14.5 - Exercises

Question 1

Write a program that takes a number from a command-line argument and print out the multiples of that number up to 10. The user should also be able to specify a formatting flag and/or shortening flag at the command line. As an example take a look at a couple of different ways a user could run the program:

$ py times_tables.py 15
0 * 15 =  0
1 * 15 =  15
2 * 15 =  30
3 * 15 =  45
4 * 15 =  60
5 * 15 =  75
6 * 15 =  90
7 * 15 =  105
8 * 15 =  120
9 * 15 =  135
10 * 15 =  150
$ py times_tables.py -f 15
 0 * 15 =   0
 1 * 15 =  15
 2 * 15 =  30
 3 * 15 =  45
 4 * 15 =  60
 5 * 15 =  75
 6 * 15 =  90
 7 * 15 = 105
 8 * 15 = 120
 9 * 15 = 135
10 * 15 = 150
$ py times_tables.py -f -s 15
  0
 15
 30
 45
 60
 75
 90
105
120
135
150

In the second example, notice the -f flag and, the -s flag in the third example.

Your program should be able to handle the following situations that may raise exceptions:

This program may take some time to implement.

Question 2

In the previous chapter, we looked at Binary Search. I said during that chapter that there are certain conditions throughout the algorithm that must always hold true. Rewrite Binary Search but this time enforce that those conditions must always True by use of assertions.

Question 3

When should we use the following?



Help support the author by donating or purchasing a copy of the book (not available yet)



Previous Chapter - Next Chapter