Introduction to Python Programming. Section 12. Exceptions

12 Exceptions

12.1 Objectives

In this section you will learn:

  • Why one should write resilient software which won’t fail with wrong input data.
  • To be especially careful when working with user input data.
  • About catching exceptions with the try-except statement.
  • About ZeroDivisionError, TypeError and other types of exceptions.
  • About the built-in function assert and the AssertionError exception.
  • How to raise exceptions with the raise statement.
  • The full form of the try-except-else-finally statement.

12.2 Why learn about exceptions?

Imagine that you are responsible for a software module which processes user data. The data is coming from another software module or from the user directly. Part of the data is a list of ages of a group of people, and you need to calculate their average. This is easy:

  def average(L):
      ~~~
      Calculate average of the numbers in the list L.
      ~~~
      a = 0
      for x in L:
          a += x
      return a / len(L)

A quick test to confirm that the function works correctly:

  ages = [1, 2, 6]
  print(average(ages))

  3.0

It is a mistake of novices to think that the story ends here. On the contrary, merely it begins here. You won’t get commended for writing a simple function like this. But you will get a lot of complaints when your function fails. Let’s show some examples. First, what if the list ages happens to arrive empty?

  ages = []
  print(average(ages))

  Traceback (most recent call last):
    File ~<string>~, line 11, in <module>
    File ~<nclab>~, line 8, in average
  ZeroDivisionError: division by zero

Oops, not good. Of course this is not your fault but life is not fair. Next, what if some of the ages arrive as text strings (although they shouldn’t)?

  ages = [1, ’2’, 6]
  print(average(ages))

  Traceback (most recent call last):
    File ~<string>~, line 1, in <module>
    File ~<nclab>~, line 7, in average
  TypeError: unsupported operand type(s) for +=: ’int’ and ’str’

Sometimes, unbelievable things can happen in computer programming – more precisely, they sound unbelievable until you get used to them. Such as, a colleague or a user who is supposed to provide a list can send you something which is not a list at all. Let’s see what happens if your function receives a dictionary. After all, these should be the ages of people, right?

  ages = {’Peter’:23, ’Paul’:16}
  print(average(ages))

  Traceback (most recent call last):
    File ~<string>~, line 1, in <module>
    File ~<nclab>~, line 4, in average
  TypeError: unsupported operand type(s) for +=: ’int’ and ’str’

Or, somebody will provide an integer instead of a list by mistake:

  ages = 1
  print(average(ages))

  Traceback (most recent call last):
    File ~<string>~, line 1, in <module>
    File ~<nclab>~, line 3, in average
  TypeError: ’int’ object is not iterable

Guess who will be blamed if a customer decides against buying the software because of a few crashes which occurred in a function you wrote. Nobody will ask what caused them. There is a famous saying in computer science which has a lot of truth to it:

"A good programmer is someone who looks both ways before crossing a one-way street."
(Doug Linder)

On the other hand, the good news is that Python makes it easy to fortify your software (and protect yourself) by catching and handling exceptions without letting the program crash. That’s what we want to show you in this section.

12.3 Catching exceptions – statement try-except

Let’s stay with the previous example for a while. One could try to anticipate all things that might go wrong, but the users are always one step ahead in their creativity. Therefore, Python provides exceptions whose philosophy is the opposite – instead of worrying in advance, let the code run and only take a corrective action if something goes wrong. We have seen in Subsection 12.2 that ZeroDivisionError and TypeError can be expected. Therefore, let’s fortify the function average against them:

  def average(L):
      ~~~
      Calculate average of the numbers in the list L.
      Return False if the calculation fails.
      ~~~
      try:
          a = 0
          for x in L:
              a+=x
          return a / len(L)
      except ZeroDivisionError:
          print(’Warning: ZeroDivisionError in average().’)
          print(’Returning False.’)
          return False
      except TypeError:
          print(’Warning: TypeError in average().’)
          print(’Returning False.’)
          return False

In the above program, the original code is in the try branch. This branch can be followed by as many except branches as you wish, that will handle different sorts of problems. Of course none of them will be executed if the try branch passes without generating an exception. We will give a list of the most frequently used exceptions in the following subsection.

It is needless to say that the function has improved. It works as before when the input data is correct:

  ages = [1, 2, 6]
  print(average(ages))

  3.0

But with missing input data it does not crash anymore! Instead, it issues a warning and returns False:

  ages = []
  print(average(ages))

  Warning: ZeroDivisionError occured in average().
  Returning False.
  False

The same if the data contain items of a wrong type:

  ages = [1, ’2’, 6]
  print(average(ages))

  Warning: TypeError occured in average().
  Returning False.
  False

Next let’s look at various other types of exceptions that one can catch.

12.4 List of common exceptions

Here is a list of the most frequently used exceptions for reference:

  • Exception: Base class for all exceptions. It can be used to catch a general exception.This will be discussed in Subsection 12.5.
  • StopIteration: Raised when the next method of an iterator does not point to any object. For iterators see Subsection 9.13.
  • StandardError Base class for all built-in exceptions except StopIteration and SystemExit.
  • ArithmeticError Base class for all errors that occur for numeric calculation.
  • OverflowError Raised when a calculation exceeds maximum limit for a numeric type.
  • FloatingPointError Raised when a floating point calculation fails.
  • ZeroDivisionError Raised when division or modulo by zero takes place for all numeric types.
  • AssertionError Raised in case of failure of the assert statement. This will be discussed in Subsection 12.6.
  • AttributeError Raised in case of failure of attribute reference or assignment.
  • EOFError Raised when there is no input from either the raw_input or input function and the end of file is reached.
  • ImportError Raised when an import statement fails.
  • KeyboardInterrupt Raised when the user interrupts program execution, usually by pressing CTRL+C.
  • LookupError Base class for all lookup errors.
  • IndexError Raised when an index is not found in a sequence.
  • KeyError Raised when the specified key is not found in the dictionary.
  • NameError Raised when an identifier is not found in the local or global namespace.
  • UnboundLocalError Raised when trying to access a local variable in a function or method but no value has been assigned to it.
  • EnvironmentError Base class for all exceptions that occur outside the Python environment.
  • IOError Raised when an input/ output operation fails, such as the print statement or the open function when trying to open a file that does not exist.
  • OSError Raised for operating system-related errors.
  • SyntaxError Raised when there is an error in Python syntax.
  • IndentationError Raised when indentation is not specified properly.
  • SystemError Raised when the interpreter finds an internal problem, but when this error is encountered the Python interpreter does not exit.
  • SystemExit Raised when Python interpreter is quit by using the sys.exit function. If not handled in the code, causes the interpreter to exit.
  • TypeError Raised when an operation or function is attempted that is invalid for the specified data type.
  • ValueError Raised when the built-in function for a data type has the valid type of arguments, but the arguments have invalid values specified.
  • RuntimeError Raised when a generated error does not fall into any category.
  • NotImplementedError Raised when an abstract method that needs to be implemented in an inherited class is not actually implemented.

12.5 Catching a general exception

If one does not need to perform different corrective actions based on different types of exceptions, it is often enough to only catch the general exception:

  def average(L):
      ~~~
      Calculate average of the numbers in the list L.
      Return False if the calculation fails.
      ~~~
      try:
          a = 0
          for x in L:
              a+=x
          return a / len(L)
      except Exception as e:
          print(’Warning in average(): ’ + str(e))
          print(’Returning False.’)
          return False

When called with missing input data, the function now responds as follows:

  ages = []
  print(average(ages))

  Warning in average(): division by zero
  Returning False.
  False

And when the data is wrong:

  ages = [1, ’2’, 6]
  print(average(ages))

  Warning in average(): unsupported operand type(s) for +=:
  ’int’ and ’str’
  Returning False.
  False

12.6 Function assert

The built-in function assert raises an exception when the Boolean statement used as its argument is False. This is a quick way to make sure that the program does not continue with incorrect data:

  def myfunction(x):
      ~~~
      Returns 1/x. If x == 0, throws AssertionError.
      ~~~
      assert(x != 0)
      return 1/x
  
  print(myfunction(0))

  Traceback (most recent call last):
    File ~<string>~, line 8, in <module>
    File ~<nclab>~, line 5, in myfunction
  AssertionError

Alternatively, the AssertionError exception can be caught using the try command as usual:

  import numpy as np
  def myfunction(x):
      ~~~
      Returns 1/x. If x == 0, returns np.nan.
      ~~~
      try:
          assert(x != 0)
          return 1/x
      except AssertionError:
          print(’Warning in myfunction(): division by zero’)
          print(’Returning nan.’)
          return np.nan
  
  print(myfunction(0))

Warning in myfunction(): division by zero  
Returning nan.  
nan

12.7 Throwing exceptions – statement raise

Python does not have a command to stop program execution. If you need to terminate your program prematurely, for example because of wrong input data, the best thing is to raise an exception using the raise statement.

Let’s illustrate this on a function which is undefined for x = 3 and x = -3:

  def f(x):
      ~~~
      Returns 1/(x**2 - 9).
      If x == 3 or x == -3, raises ValueError.
      ~~~
      if x == 3 or x == -3:
          raise ValueError(Division by zero detected.)
      return 1 / (x**2 - 9)

When the function f(x) is called with 3 or -3, the output will be

  Traceback (most recent call last):
    File ~<string>~, line 9, in <module>
    File ~<nclab>~, line 6, in myfunction
  ValueError: Division by zero detected.

12.8 The full statement try-except-else-finally

You already know from Subsection 12.3 that the try-except statement can contain multiple except branches. Moreover, it can contain optional branches else and/or finally. Your understanding of the else branch in for loops (Subsection 9.12) and in while loops (Subsection 11.8) will make this very simple.

The else branch: The code is executed only if no exceptions were raised in the try branch. This is analogous to for-else and while-else where the else branch is executed only if no break was used. Code executed in this branch is just like normal code: if there is an exception, it will not be automatically caught (and probably stop the program).

The finally branch: The code always executes after the other blocks, even if there was an uncaught exception (that didnt cause a crash) or a return statement in one of the other branches. Code executed in this branch is just like normal code: if there is an exception, it will not be automatically caught.


Table of Contents

Created on August 6, 2018 in Python I,   Python II.
Add Comment
0 Comment(s)

Your Comment

By posting your comment, you agree to the privacy policy and terms of service.