Linguist 278: Programming for Linguists (Stanford Linguistics, Fall 2021)

Class 14: Exceptions

Exceptions are errors that arise when programs are run. (This distinguishes them from mere syntax errors, which can be caught before programs are run.)

Exceptions are powerful tools that you can use to write better programs.

The semantics of exceptions

Python has a lot of built-in exceptions with semantic guidelines that you should try to follow when raising exceptions in your own programs.

Raising exceptions

The raise built-in lets you raise specific exceptions:

def palindrome_detector(s):
    if not isinstance(s, str):
        raise TypeError("palindrome_detector expects type str")
    s = s.lower().replace(" ", "")
    return s == s[::-1]

Using assert

The assert built-in can be used to raise an AssertionError. It is very commonly used in writing unit tests:

def test_palindrome_detector():
    ex = 'Lisa Bonet ate no basil'
    expected = True    
    result = palindrome_detector(ex)
    assert result == expected, "For '{}', expected {}; got {}".format(ex, expected, result)

You can think of the final line as a short version of

if result != expected:
    raise AssertionError("For '{}', expected {}; got {}".format(ex, expected, result))

Exception handling

There will be situations in which you expect your program to raise exceptions and you want to handle them rather than letting them crash the program.

Basic try ... except

For example, the following code seems to make the assumption that the input consists of a list of things that can be turned into an int, and it uses exception handling to tell the user about any instances in which that assumption proves false.

import warnings

def int_converter(vals):
    new_vals = []
    for x in vals:
        try:
            x = int(x)
            new_vals.append(x)
        except ValueError:
            warnings.warn("Couldn't convert {}".format(x))        
    return new_vals

Note: I specified the exception that I expect to see (ValueError). This is optional, but it is always good to do, so that your program does stop if something truly unexpected happens. If there are multiple errors that you want to handle, then you can give them as a tuple (e.g., (TypeError, ValueError).)

else clauses

If you want something specific to happen where no exception is raised, you can optionally include an else clause:

def int_converter_else(vals):
    new_vals = []
    for x in vals:
        try:
            x = int(x)
        except ValueError:
            warnings.warn("Couldn't convert {}".format(x))
        else:            
            new_vals.append(x)
    return new_vals

The above is probably better style than int_converter, since one really want to do as little as possible inside the try block so that it is easier to ensure that any exceptions raised really have the source that you expect.

finally clauses

Just to round this out, there is also an optional finally clause, where you can specify code that will execute whether an exception was raised or not:

def int_converter_else_finally(vals):
    new_vals = []
    for x in vals:
        try:
            x = int(x)
        except ValueError:
            warnings.warn("Couldn't convert {}".format(x))
        else:            
            new_vals.append(x)
        finally:
            print("Handled {}".format(x))
    return new_vals

Deliberate exceptions

Here's an example of exception handling used as a strategy for adding new keys to a dictionary:

def counter_tryexcept(vals):
    d = {}
    for x in vals:
        try:
            d[x] += 1
        except KeyError:
            d[x] = 1
    return d

This might be the fastest way to implement a simple count dictionary!