from IPython.core.display import HTML

HTML(open("custom.html", "r").read())
Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright (C) 2014-2023 Scientific IT Services of ETH Zurich,
Contributing Authors: Uwe Schmitt, Mikolaj Rybniski

10. Handling errors - Python Exceptions¶

In Python error conditions are handled using so called "exceptions". This is an example for an exception:

x = 1 / 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[2], line 1
----> 1 x = 1 / 0

ZeroDivisionError: division by zero

The default mode is that such an exception stops program execution.

As a Python programmer we can change this behavior by catching an exception

try:
    x = 1 / 0
except ZeroDivisionError:
    print("only chuck norris can divide by zero!")
only chuck norris can divide by zero!

This will only catch the zero division error. Any other error which could occur between try and except will not be caught:

try:
    x = y / 0
except ZeroDivisionError:
    print("only chuck norris can divide by zero!")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 2
      1 try:
----> 2     x = y / 0
      3 except ZeroDivisionError:
      4     print("only chuck norris can divide by zero!")

NameError: name 'y' is not defined

You can chain exception handling, in case you want to handle different exceptions in a different way:

try:
    # y is not defined here and we divide by zero:
    x = y / 0
except ZeroDivisionError:
    print("only chuck norris can divide by zero!")
except NameError:
    print("what ??")
what ??

you can also use the same handler for multiple exceptions:

try:
    x = y / 0
except (ZeroDivisionError, NameError):
    print("oops")
oops

If an exception is triggerend within a nested function call we see the call stack:

def divide(a, b):
    return a / b


def inverse(a):
    return divide(1, a)


print(inverse(0))
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[7], line 9
      5 def inverse(a):
      6     return divide(1, a)
----> 9 print(inverse(0))

Cell In[7], line 6, in inverse(a)
      5 def inverse(a):
----> 6     return divide(1, a)

Cell In[7], line 2, in divide(a, b)
      1 def divide(a, b):
----> 2     return a / b

ZeroDivisionError: division by zero

You see in the output above that the previous script called inverse(0) which then called divide(1, 0) which finally caused the ZeroDivisionError.

The exception "bubbles" the up the call stack until the top level, and as it is not caught the stack-trace is printed.

We can intercept this at any point of the call stack:

def divide(a, b):
    return a / b


def inverse(a):
    try:
        return divide(1, a)
    except ZeroDivisionError:
        return None


print(inverse(0))
None

It is easier to ask for forgiveness than for permission¶

To check if a given string represents a number, we can either come up with a solution based an analysing the given string character by character, or we use exception handling:

def is_float(string):
    try:
        float(string)
    except ValueError:
        return False
    return True


print(is_float("1.2"))
print(is_float("1.ab"))
True
False

Raising exceptions¶

Exceptions can also be risen on demand to indicate error conditions:

def fun(number):
    if number < 0.0:
        raise ValueError(f"{number=} is negative! ")
    return number
print(fun(1.0))
1.0
# read the output below line by line !!!
print(fun(-1.0))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[12], line 2
      1 # read the output below line by line !!!
----> 2 print(fun(-1.0))

Cell In[10], line 3, in fun(number)
      1 def fun(number):
      2     if number < 0.0:
----> 3         raise ValueError(f"{number=} is negative! ")
      4     return number

ValueError: number=-1.0 is negative! 

To do processing and re-raise a possibly unknown error, save it first using the as keyword:

try:
    divide_by_zero(1)
except ZeroDivisionError:
    print("Infinity!")
except Exception as e:
    print("Opps, fallback! Smth went wrong: ", e)
    raise e
Opps, fallback! Smth went wrong:  name 'divide_by_zero' is not defined
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[13], line 7
      5 except Exception as e:
      6     print("Opps, fallback! Smth went wrong: ", e)
----> 7     raise e

Cell In[13], line 2
      1 try:
----> 2     divide_by_zero(1)
      3 except ZeroDivisionError:
      4     print("Infinity!")

NameError: name 'divide_by_zero' is not defined

Why exceptions at all ?¶

  • Before exceptions were introduced in programming languages like C++, programmers used e.g. special return values to indicate error conditions.
  • This only works if the domain of reasonable return values allows special values.
  • This also requires that such error conditions are checked by the caller.
  • Exceptions avoid propagation of invalid results if error handling is forgotten.
  • Exceptions make a clear distinction between return value and error conditions.
  • Exceptions force the programmer to think about possible errors and to implement strategies for handling them.

Builtin exceptions, exception hierarchies¶

These are all exceptions available per default in Python, you can also see that there is a hierarchy:

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

So in the diagram, you can see that the depicted hierarchy ZeroDivisionError is below ArithmeticError. This means we also could handle 1 / 0 using:

try:
    1 / 0
except ArithmeticError:
    print("oops")
oops

The drawback here is that this also catches FloatingPointError and OverflowError.

WARNING: Don't catch the Exception or even BaseException without re-raising them. This will catch also any programming mistake you make and will make debugging very difficult.

You can also create your own exceptions by sub-classing from a given exception (details about sub-classing are in the other script about object oriented programming):

class MyNumericalError(ArithmeticError):

    pass


raise MyNumericalError("don't like numbers")
---------------------------------------------------------------------------
MyNumericalError                          Traceback (most recent call last)
Cell In[15], line 6
      1 class MyNumericalError(ArithmeticError):
      3     pass
----> 6 raise MyNumericalError("don't like numbers")

MyNumericalError: don't like numbers

The finally keyword¶

In addition to except one can also declare actions to be executed in error cases as well in situation where code works as it should. This is done using finally:

def div(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None
    finally:
        print("div done")


div(1, 2)
div(1, 0)
div done
div done

Assertions¶

A simple way to check some conditions and to raise an exeption if this condition is not met, is the assert statement:

def square_root(x):
    assert x >= 0, "does not work with negative values"
    return x**0.5


print(square_root(4))
print(square_root(-1))
2.0
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[17], line 7
      3     return x**0.5
      6 print(square_root(4))
----> 7 print(square_root(-1))

Cell In[17], line 2, in square_root(x)
      1 def square_root(x):
----> 2     assert x >= 0, "does not work with negative values"
      3     return x**0.5

AssertionError: does not work with negative values

see also https://docs.python.org/3/tutorial/errors.html¶

Exercise section¶

  1. Write a function which first tests if a string represents an integer, then if it is a float. Return "int", "float" or "str" according to the the three cases.

Optional exercise*¶

  1. Write a function which takes a string of form N1 op N2 where N1 and N2 are numbers and op is one of +, -, /, * and **. You can assume spaces around op. The function splits the string and evaluates the final result. Catch exceptions for all numerical computations involved. Transform all such exceptions as well as format errors in the input (no spaces, no numbers, invalid operation, ...) to a InvalidTerm exception which you have to implement on your own.