Python: A tale of two styles of avoiding or handling errors

There are two distinct approaches to error handling (lots of ifstatements vs. catching exceptions). These approaches are respectively known as Look Before You Leap (LBYL) and Easier to Ask for Forgiveness than Permission. In the LBYL camp, you always check to see if something can be done before doing it. In EAFP, you just do the thing. If it turns out that wasn’t possible, shrug “my bad”, and deal with it.

Idiomatic Python is written in the EAFP style (where reasonable). We can do so because exceptions are cheap in Python.

LBYL vs. EAFP

It’s all well and good that exceptions are widely used in core Python constructs, but why is a different question. After all, for could certainly have been written to not rely on exceptions to mark the end of a sequence. Indeed, exceptions could have been avoided altogether.

But they exist due to the philosophical approach to error checking adopted in Python. Code that doesn’t use exceptions is always checking if it’s OK to do something. In practice, it must ask a number of different questions before it is convinced it’s OK to do something. If it doesn’t ask all of the right questions, bad things happen. Consider the following code:

def print_object(some_object):
    # Check if the object is printable...
    if isinstance(some_object, str):
        print(some_object)
    elif isinstance(some_object, dict):
        print(some_object)
    elif isinstance(some_object, list):
        print(some_object)
    # 97 elifs later...
    else:
        print("unprintable object")

This trivial function is responsible for calling print() on an object. If it can’t be print()-ed, it prints an error message.

Trying to anticipate all error conditions in advance is destined for failure (and is also really ugly). Duck typing is a central idea in Python, but this function will incorrectly print an error for types than can be printed but aren’t explicitly checked.

The function can be rewritten like so:

def print_object(some_object):
    # Check if the object is printable...
    try:
        printable = str(some_object)
        print(printable)
    except TypeError:
        print("unprintable object")

If the object can be coerced to a string, do so and print it. If that attempt raises an exception, print our error string. Same idea, much easier to follow (the lines in the try block could obviously be combined but weren’t to make the example more clear). Also, note that we’re explicitly checking for TypeError, which is what would be raised if the coercion failed. Never use a “bare” except: clause or you’ll end up suppressing real errors you didn’t intend to catch.

But wait, there’s more!

The function above is admittedly contrived (though certainly based on a common anti-pattern). There are a number of other useful ways to use exceptions. Let’s take a look at the use of an else clause when handling exceptions.

In the rewritten version of print_object below, the code in the elseblock is executed only if the code in the try block didn’t throw an exception. It’s conceptually similar to using else with a for loop (which is itself a useful, if not widely known, idiom). It also fixes a bug in the previous version: we caught a TypeError assuming that only the call to str()would generate it. But what if it was actually (somehow) generated from the call to print() and has nothing to do with our string coercion?

def print_object(some_object):
    # Check if the object is printable...
    try:
        printable = str(some_object)
    except TypeError:
        print("unprintable object")
    else:
        print(printable)

Now, the print() line is only called if no exception was raised. If print()raises an exception, this will bubble up the call stack as normal. The elseclause is often overlooked in exception handling but incredibly useful in certain situations. Another use of else is when code in the try block requires some cleanup (and doesn’t have a usable context manager), as in the below example:

def display_username(user_id):
    try:
        db_connection = get_db_connection()
    except DatabaseEatenByGrueError:
        print('Sorry! Database was eaten by a grue.')
    else:
        print(db_connection.get_username(user_id))
        db_connection.cleanup()

How not to confuse your users

A useful pattern when dealing with exceptions is the bare raise. Normally, raise is paired with an exception to be raised. However, if it’s used in exception handling code, raise has a slightly different (but immensely useful) meaning.

def calculate_value(self, foo, bar, baz):
    try:
        result = self._do_calculation(foo, bar, baz)
    except:
        self.user_screwups += 1 
        raise
    return result

Here, we have a member function doing some calculation. We want to keep some statistics on how often the function is misused and throws an exception, but we have no intention of actually handling the exception. Ideally, we want to an exception raised in _do_calculation to be flow back to the user code as normal. If we simply raised a new exception from our except clause, the traceback point to our except clause and mask the real issue (not to mention confusing the user). raise on its own, however, lets the exception propagate normally with its original traceback. In this way, we record the information we want and the user is able to see what actually caused the exception.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s