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)
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 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.