CptS 481 - Python Software Construction

Unit 10: Exceptions and Warnings

In [2]:
from IPython.display import HTML
HTML(open("../include/notes.css", "r").read())
Out[2]:
In [11]:
allowExceptions = True # set to `False` for Jupyter "Run All" to work

Exceptions

The Built-in Exceptions
Background: The Ackermann Function

Here's a simple function known as the "Ackermann" (technically "Ackermann–Péter") function. It comes from computability theory and was proposed in 1928. We've added an optional trace argument so we can see how it's called.

In [12]:
def ackermann(m, n, trace=False):
    if m == 0:
        result = n+1
    elif m > 0 and n == 0:
        result = ackermann(m-1, 1, trace)
    else:
        assert m > 0 and n >= 0
        result = ackermann(m-1, ackermann(m, n-1, trace), trace)
    if trace:
        print("ackermann({}, {}) -> {}".format(m, n, result))
    return result

Looks simple. Let's try it.

In [13]:
print(ackermann(0, 1))
2
In [14]:
print(ackermann(1, 0))
2
In [15]:
print(ackermann(2, 0))
3

Now let's trace the result:

In [16]:
print(ackermann(2, 0, True)) # False->True to trace the result
ackermann(0, 1) -> 2
ackermann(1, 0) -> 2
ackermann(0, 2) -> 3
ackermann(1, 1) -> 3
ackermann(2, 0) -> 3
3

Simple, huh? So let's try m of 4.

In [17]:
print(ackermann(4, 0))
13

A lot more recursion, but no real problems. Now, let's try n of 1. (Warning: If you try this at home, be sure not to set trace to True.)

Stand back! This may take a while!

In [18]:
if allowExceptions:
    print(ackermann(4, 1))
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-18-8d1f483e9b45> in <module>
      1 if allowExceptions:
----> 2     print(ackermann(4, 1))

<ipython-input-12-b20f5e83be05> in ackermann(m, n, trace)
      6     else:
      7         assert m > 0 and n >= 0
----> 8         result = ackermann(m-1, ackermann(m, n-1, trace), trace)
      9     if trace:
     10         print("ackermann({}, {}) -> {}".format(m, n, result))

... last 1 frames repeated, from the frame below ...

<ipython-input-12-b20f5e83be05> in ackermann(m, n, trace)
      6     else:
      7         assert m > 0 and n >= 0
----> 8         result = ackermann(m-1, ackermann(m, n-1, trace), trace)
      9     if trace:
     10         print("ackermann({}, {}) -> {}".format(m, n, result))

RecursionError: maximum recursion depth exceeded in comparison

What's going on here? Looks like we can only compute some values of ackermann(m,n), but we may not know which ones before run time. How can we prevent the program from blowing up like this?

Handling Exceptions: The try ... except Statement
In [19]:
try:
    y = 3
    raise OverflowError()
except (SyntaxError, RuntimeError, OverflowError):
    print("exception detected")
    raise
print("that's all")
exception detected
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
<ipython-input-19-6cb02ae56f8a> in <module>
      1 try:
      2     y = 3
----> 3     raise OverflowError()
      4 except (SyntaxError, RuntimeError, OverflowError):
      5     print("exception detected")

OverflowError: 
In [20]:
def arbitraryFunction():
    raise SyntaxError

try:
    y = 3
    arbitraryFunction()
except (SyntaxError, OverflowError):
    print("exception detected")
    raise
print("that's all")
exception detected
Traceback (most recent call last):

  File "/usr/local/lib/python3.8/dist-packages/IPython/core/interactiveshell.py", line 3417, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)

  File "<ipython-input-20-9cf1bd3fa17c>", line 6, in <module>
    arbitraryFunction()

  File "<ipython-input-20-9cf1bd3fa17c>", line 2, in arbitraryFunction
    raise SyntaxError

  File "<string>", line unknown
SyntaxError
In [21]:
arbitraryFunction()
Traceback (most recent call last):

  File "/usr/local/lib/python3.8/dist-packages/IPython/core/interactiveshell.py", line 3417, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)

  File "<ipython-input-21-c1f2f467c997>", line 1, in <module>
    arbitraryFunction()

  File "<ipython-input-20-9cf1bd3fa17c>", line 2, in arbitraryFunction
    raise SyntaxError

  File "<string>", line unknown
SyntaxError

syntax (simplified):

try:
    {trySuite}
except {exceptionName(s)}:
    {exceptionSuite}

{trySuite} is a suite of code that may cause an exception to be raised. {exceptionName(s)} is an exception name or tuple of exception names. {exceptionSuite} is a suite of code that will be executed when one of the exceptions is raised.

So let's use this to "wrap" an ackermann() call in a testing framework.

In [22]:
def testAckermann(m, n):
    try:
        print("ackermann({}, {}) = {}".format(m, n, ackermann(m, n)))
    except RecursionError:
        print("ackermann({}, {}) = ?".format(m, n))

testAckermann(4, 1)
ackermann(4, 1) = ?

Exercise for the astute: Write a function checkForRuntimeError(func, *args, **kwargs) that will test any function func() (passing *args and **kwargs to it) and return True iff it does not cause a RuntimeError to be raised.

When Should You Catch an Exception?

Uncaught exceptions cause the program to exit with lots of information about where the exception happened. Often, that's what you want to have happen.

Only catch an exception when you want to do one or more of the following:

  • treat the condition as something other than a bug

  • recover from the condition and let the program proceed

    (This is especially useful for testing.)

  • provide more information about the bug than the default

Raising Exceptions: The raise Statement

In the above, the interpreter raised the RuntimeError exception itself, but our code can raise exceptions explicitly with the raise statement:

raise {exceptionName}

We'll use this in an example below.

We can pass an object {messageObject} as a message to the exception (constructor):

raise {exceptionName}({messageObject})

and if we have

raise

all by itself in an exception handler, the current exception will be passed up the calling stack to the next higher handler. This is like "throw;" in a C++ exception handler and something much more complicated in a Java exception handler.

Tidying Up: The try ... finally Clause

syntax:

try:
    {trySuite}
finally:
    {finallySuite}

{finallySuite} is executed whenever {trySuite} passes out of the calling hierarchy, including:

  • a normal (un-"exception"-al) end

  • a break (from a surrounding loop)

  • a continue (from a surrounding loop)

  • a return (from the current function)

  • an exception (even matching the same try)

The assert Statement

Think of assert as a "sanity check" for your code. It raises the AssertionError exception. The syntax is:

assert {condition}[, {expression}]

If {condition} is false, the exception is raised and {expression} is used as the error message. Note: {expression} can be any object, including a tuple of multiple values.

In [23]:
def fact(n):
    assert n >= 0, 'negative argument'
    if n == 0:
        return 1
    else:
        return n * fact(n-1)

print(fact(-300))
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-23-fb49b1e639a7> in <module>
      6         return n * fact(n-1)
      7 
----> 8 print(fact(-300))

<ipython-input-23-fb49b1e639a7> in fact(n)
      1 def fact(n):
----> 2     assert n >= 0, 'negative argument'
      3     if n == 0:
      4         return 1
      5     else:

AssertionError: negative argument

Creating Custom Exception Classes

Remember the Interval class? What happens to Interval.__div__(self, other) when other spanned 0? What happens when we compare two intervals that overlap?

Stand back: It's time to use an Exception!

In [25]:
class IntervalRangeError(Exception):
    pass

class IntervalComparisonError(Exception):
    pass

class Interval:

    def __init__(self, min=0, max=None):
        if max == None:
            max = min
        if min > max:
            raise IntervalRangeError
        self.min = min
        self.max = max

    def __add__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        return Interval(self.min+y.min, self.max+y.max)

    def __lt__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        if self.max < y.min:
            return True
        elif y.max < self.min:
            return False
        else:
            raise IntervalComparisonError

    def __sub__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        return Interval(self.min-y.min, self.max-y.max)

    def __pow__(self, expo):
        if expo > 0:
            return Interval(self.min**expo, self.max**expo)
        else:
            # use division to do the negative power, in case of zero-spanning
            return Interval(1,1)/Interval(self.min**(-expo), self.min**(-expo))

    def __mul__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        extrema = [self.min*y.min, self.min*y.max,
                self.max*y.min, self.max*y.max]
        return Interval(min(extrema), max(extrema))

    def __truediv__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        if y.min <= 0 <= y.max:
            raise ZeroDivisionError
        extrema = [self.min/y.min, self.min/y.max,
                self.max/y.min, self.max/y.max]
        return Interval(min(extrema), max(extrema))

    def __repr__(self):
        return "[%f..%f]" % (self.min, self.max)

if __name__ == '__main__':
    a = Interval(0.1, 0.25)
    b = Interval(0.125, 0.185)
    c = Interval(3)
    d = Interval(-1,1)
    print('Test of the interval arithmetic module:')
    print('          a =', a)
    print('          b =', b)
    print('          c =', c)
    print('          d =', d)
    print('        a+b =', a+b)
    print('        a-b =', a-b)
    print('        a*b =', a*b)
    print('        a/b =', a/b)
    print('       a**2 =', a**2)
    print('  a**(-0.5) =', a**(-0.5))
    print('(a*b)/(a+b) =', (a*b)/(a+b))
    try:
        e = Interval(1)/d
    except ZeroDivisionError:
        print("division by interval containing zero was caught")
    try:
        if a < c:
            print("less-than comparison works")
        if c > a:
            print("greater-than comparison works")
        if a < d:
            pass  # should trigger exception
    except IntervalComparisonError:
        print("interval comparison exception handled")
Test of the interval arithmetic module:
          a = [0.100000..0.250000]
          b = [0.125000..0.185000]
          c = [3.000000..3.000000]
          d = [-1.000000..1.000000]
        a+b = [0.225000..0.435000]
        a-b = [-0.025000..0.065000]
        a*b = [0.012500..0.046250]
        a/b = [0.540541..2.000000]
       a**2 = [0.010000..0.062500]
  a**(-0.5) = [3.162278..3.162278]
(a*b)/(a+b) = [0.028736..0.205556]
division by interval containing zero was caught
less-than comparison works
greater-than comparison works
interval comparison exception handled

Exceptions are classes, and like classes they can be arranged in a hierarchy. When parent and child exceptions have distinct exception suites, Python will always choose the most specific (i.e., the child).

We can also give exceptions special methods like __repr__() that Python will use when needed.

In [36]:
class IntervalError(Exception):
    pass    

class IntervalRangeError(IntervalError):
    pass

class IntervalComparisonError(IntervalError):
    def __init__(self):
        pass
    def __repr__(self):
        return 'IntervalComparisonError class __repr__() called'

class Interval(object):

    def __init__(self, min=0, max=None):
        if max == None:
            max = min
        if min > max:
            raise IntervalRangeError
        self.min = min
        self.max = max

    def __add__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        return Interval(self.min+y.min, self.max+y.max)

    def __lt__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        if self.max < y.min:
            return True
        elif y.max < self.min:
            return False
        else:
            raise IntervalComparisonError

    def __sub__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        return Interval(self.min-y.min, self.max-y.max)

    def __pow__(self, expo):
        if expo > 0:
            return Interval(self.min**expo, self.max**expo)
        else:
            # use division to do the negative power, in case of zero-spanning
            return Interval(1,1)/Interval(self.min**(-expo), self.max**(-expo))

    def __mul__(self, y):
        if not isinstance(y, Interval):
            raise TypeError
        extrema = [self.min*y.min, self.min*y.max,
                self.max*y.min, self.max*y.max]
        return Interval(min(extrema), max(extrema))

    def __truediv__(self, y):
        if type(y) == int:
            y = Interval(y, 1)
        if not isinstance(y, Interval):
            raise TypeError
        if y.min <= 0 <= y.max:
            raise ZeroDivisionError
        extrema = [self.min/y.min, self.min/y.max,
                self.max/y.min, self.max/y.max]
        return Interval(min(extrema), max(extrema))

    def __repr__(self):
        return "[%f..%f]" % (self.min, self.max)

a = Interval(0.1, 0.25)
b = Interval(0.125, 0.185)
c = Interval(3)
print('Test of the interval arithmetic module:')
print('          a =', a)
print('          b =', b)
print('          c =', c)
print('        a+b =', a+b)
print('        a-b =', a-b)
print('        a*b =', a*b)
print('        a/b =', a/b)
print('       a**2 =', a**2)
print('  a**(-0.5) =', a**(-0.5))
print('(a*b)/(a+b) =', (a*b)/(a+b))

print()
d = Interval(-1,1)
try:
    e = Interval(1)/d
except ZeroDivisionError:
    print("division by interval containing zero was caught")

print()
try:
    Interval(1, -1)
except IntervalRangeError:
    print("IntervalRangeError exception caught")

print()
try:
    if a < c:
        print("less-than comparison works")
    if c > a:
        print("greater-than comparison works")
    if a < d:
        pass  # should trigger exception
except IntervalComparisonError:
    if 0:
        print("interval comparison exception re-raised")
        raise

print()
try:
    if a < d:
        pass  # should trigger exception
except IntervalError:
    print("interval comparison superclass exception handled")
Test of the interval arithmetic module:
          a = [0.100000..0.250000]
          b = [0.125000..0.185000]
          c = [3.000000..3.000000]
        a+b = [0.225000..0.435000]
        a-b = [-0.025000..0.065000]
        a*b = [0.012500..0.046250]
        a/b = [0.540541..2.000000]
       a**2 = [0.010000..0.062500]
  a**(-0.5) = [2.000000..3.162278]
(a*b)/(a+b) = [0.028736..0.205556]

division by interval containing zero was caught

IntervalRangeError exception caught

less-than comparison works
greater-than comparison works

interval comparison superclass exception handled
A Less Boring Example: madlibs

Here's a piece of code that is, in the classic sense of "Pythonic", too silly. Suppose we wanted to create an on-line version of the party game "madlibs", where users provide specific instances of generic words like "adjective" or "name".

In [29]:
text = """
{adjective0}ly {adjective1} Sir {name} went forth from {town}.
           He was not at all {adjective2} to {verb0},
               O {adjective0} Sir {name}.
He was not at all {adjective2} to be {verb1}ed in {adjective3} ways,
  {adjective0}, {adjective0}, {adjective0}, {adjective0} Sir {name}!
"""[1:]

# The user might enter these in an HTML form...
userEntries = {
    'name': 'Nigel',
    'adjective0': 'bumbling',
    'adjective1': 'clumsy',
    'adjective2': 'foolish',
    'adjective3': 'silly',
    'town': 'Issaquah',
    'verb0': 'croak',
    'verb1': 'smash'
}

print(text.format(**userEntries), end='')
bumblingly clumsy Sir Nigel went forth from Issaquah.
           He was not at all foolish to croak,
               O bumbling Sir Nigel.
He was not at all foolish to be smashed in silly ways,
  bumbling, bumbling, bumbling, bumbling Sir Nigel!

But there's a problem here: Words that begin a sentence (e.g. on the 1st and 5th lines) need to be capitalized, but left alone elsewhere. We can get around this by overloading the __format__() built-in for a Word class we create. This means we can capture the formatSpec in "{keyword:formatSpec}" entries that str.format() detects and use it to transform the (here, str) object however we like.

In [30]:
class Word(str):

    def __format__(self, formatSpec):
        result = self
        if len(self) > 0:
            if formatSpec == 'cap':
                result = self[0].upper() + self[1:]
            else:
                result = super(str, self).__format__(formatSpec)
        return result

text = """
  {adjective0:cap}ly {adjective1} Sir {name} went forth from {town}.
           He was not at all {adjective2} to {verb0},
                  O {adjective0} Sir {name}.
 He was not at all {adjective2} to be {verb1}ed in {adjective3} ways,
{adjective0:cap}, {adjective0}, {adjective0}, {adjective0} Sir {name}!
"""[1:]

# The user might enter these in an HTML form...
userEntries = {
          'name': Word('Nigel'),
    'adjective0': Word('bumbling'),
    'adjective1': Word('clumsy'),
    'adjective2': Word('foolish'),
    'adjective3': Word('silly'),
          'town': Word('Ellensburg'),
         'verb0': Word('croak'),
         'verb1': Word('smash')
}

print(text.format(**userEntries), end='')
  Bumblingly clumsy Sir Nigel went forth from Ellensburg.
           He was not at all foolish to croak,
                  O bumbling Sir Nigel.
 He was not at all foolish to be smashed in silly ways,
Bumbling, bumbling, bumbling, bumbling Sir Nigel!

Useful, but we can do better. Instead of adding special handling for the "cap" formatSpec, let's take advantage of the str.capitalize() (or str.anythingElse()) builtin method.

In [31]:
class Word(str):

    def __format__(self, formatSpec):
        if not formatSpec: # formatSpec defaulted
            return self
        try:
            # Try to look up the `formatSpec` by name as a string
            # method in the dictionary of the str class and apply
            # it to ``self``.
            return str.__dict__[formatSpec](self)
        except KeyError:
            # `formatSpec` is not a known method, so do conventional
            # string formatting.
            return super().__format__(formatSpec)

# We're using different keywords here to show how you can choose
# them from the original quote and substitute directly.
text = """
{brave:capitalize}ly {bold} Sir {robin:capitalize} went forth from {camelot:capitalize}.
    He was not {afraid} to {die},
O {brave} Sir {robin:capitalize}!
    He was not at all {afraid} to be {kill}ed in {nasty} ways,
{brave:capitalize}, {brave}, {brave}, {brave} Sir {robin:capitalize}!
"""[1:]

# The user might enter these in an HTML form...
userEntries = {
      'robin': Word('nigel'),
      'brave': Word('brash'),
       'bold': Word('heroic'),
     'afraid': Word('afeared'),
      'nasty': Word('ugly'),
    'camelot': Word('kennewick'),
        'die': Word('croak'),
       'kill': Word('trash'),
}

print(text.format(**userEntries), end='')
Brashly heroic Sir Nigel went forth from Kennewick.
    He was not afeared to croak,
O brash Sir Nigel!
    He was not at all afeared to be trashed in ugly ways,
Brash, brash, brash, brash Sir Nigel!

The with Statement

This is the last Python statement we will cover.

syntax:

with {contextManager}:
    {withSuite}


A "context manager" is an instance of a class with these special methods:

  • __init__(self, *args, **kwds)

    called when a {contextManager} is created (as always). It takes a self and any other arguments you care to pass.

  • __enter__(self)

    called when a {contextManager} is encountered in the with statement

  • __exit__(self, type, value, traceback)

    called when {withSuite} passes out of the calling hierarchy, just like the {finallySuite} in a "try ... finally" statement, which is actually the
    main point of using a with statement.

Here's an example of a useful context manager:

In [32]:
class XmlTag:

    def __init__(self, tag, **attrs):
        self.tag = tag
        self.attrs = attrs

    def __enter__(self):
        if self.attrs:
            print('<{}'.format(self.tag), end=' ')
            for name in self.attrs:
                print('{}="{}"'.format(name, self.attrs[name]), end=' ')
            print('>')
        else:
            print('<{}>'.format(self.tag))

    def __exit__(self, type, value, traceback):
        print('</{}>'.format(self.tag))

with XmlTag("a", link="http://www.tricity.wsu.edu", style="x"):
    print("bold text")
    if 0: # enable to see recovery from exception
        raise SyntaxError
<a link="http://www.tricity.wsu.edu" style="x" >
bold text
</a>
In [33]:
# Use variables to store contexts that are reused (and help readability).
rowTag = XmlTag('tr') # use this to start table rows
datumTag = XmlTag('td') # use this to start columns within rows
boldTag = XmlTag('b') # use this to bold-face output
with XmlTag('html'):
    with XmlTag('body'):
        with XmlTag('h1'): # header 1 style in HTML
            print("Hello, Worlds!")
        print("An Extraterrestrial's Guide to the Solar System.")
        with XmlTag('table', border=10, rules='all'):
            with rowTag: # column header tags
                with datumTag:
                    with boldTag:
                        print("planet")
                with datumTag, boldTag:
                    print("type")
                with datumTag, boldTag:
                    print("comments")
            with rowTag:
                with datumTag:
                    print("mercury")
                with datumTag:
                    print("terrestrial")
                    if 0: # enable to see what "with" does for us
                        raise RuntimeError # suppose
                with datumTag:
                    print("restaurants serve good food, but have no atmosphere")
            with rowTag:
                with datumTag:
                    print("venus")
                with datumTag:
                    print("terrestrial")
                with datumTag:
                    print("OK, if you don't mind 462 deg C")
            with rowTag:
                # alternate syntax
                with datumTag: print("earth")
                with datumTag: print("terrestrial (duh!)")
                with datumTag: print("too touristy")
            with rowTag:
                with datumTag:
                    print("mars")
                with datumTag:
                    print("terrestrial")
                with datumTag:
                    print("don't let those NASA robots see you!")
            with rowTag:
                with datumTag:
                    print("jupiter")
                with datumTag:
                    print("gas giant")
                with datumTag:
                    print("nice for you methane breathers")
            with rowTag:
                with datumTag:
                    print("saturn")
                with datumTag:
                    print("gas giant")
                with datumTag:
                    print("has a familiar ring to it")
            with rowTag:
                with datumTag:
                    print("uranus")
                with datumTag:
                    print("gas giant")
                with datumTag:
                    print("planet of the japes")
            with rowTag:
                with datumTag:
                    print("neptune")
                with datumTag:
                    print("gas giant")
                with datumTag:
                    print("the outermost planet")
                    with boldTag:
                        print("now!")
<html>
<body>
<h1>
Hello, Worlds!
</h1>
An Extraterrestrial's Guide to the Solar System.
<table border="10" rules="all" >
<tr>
<td>
<b>
planet
</b>
</td>
<td>
<b>
type
</b>
</td>
<td>
<b>
comments
</b>
</td>
</tr>
<tr>
<td>
mercury
</td>
<td>
terrestrial
</td>
<td>
restaurants serve good food, but have no atmosphere
</td>
</tr>
<tr>
<td>
venus
</td>
<td>
terrestrial
</td>
<td>
OK, if you don't mind 462 deg C
</td>
</tr>
<tr>
<td>
earth
</td>
<td>
terrestrial (duh!)
</td>
<td>
too touristy
</td>
</tr>
<tr>
<td>
mars
</td>
<td>
terrestrial
</td>
<td>
don't let those NASA robots see you!
</td>
</tr>
<tr>
<td>
jupiter
</td>
<td>
gas giant
</td>
<td>
nice for you methane breathers
</td>
</tr>
<tr>
<td>
saturn
</td>
<td>
gas giant
</td>
<td>
has a familiar ring to it
</td>
</tr>
<tr>
<td>
uranus
</td>
<td>
gas giant
</td>
<td>
planet of the japes
</td>
</tr>
<tr>
<td>
neptune
</td>
<td>
gas giant
</td>
<td>
the outermost planet
<b>
now!
</b>
</td>
</tr>
</table>
</body>
</html>

Warnings

  • The Built-in Warning Classes

  • The warnings Module

  • The traceback Module

We'll just mention these in passing. More details are in the book.

In [ ]: