This evolved from a discussion with Marcus on exactly what is best practice for exception handling in python. This is a good question, and though there are probably different camps, another good question is to decide how we’ll do it throughout rocon to maintain some readable consistency in the code.
This document is written from the perspective of a library/module developer. Some common guidelines:
A try/except block is extremely efficient if no exceptions are raised. Actually catching an exception is expensive. An example:
try:
value = mydict[key]
except KeyError:
mydict[key] = getvalue(key)
value = mydict[key]
This only makes sense when you expect the dict to have the key almost all the time. If that wasn’t the case, you code it like this:
if key in mydict:
value = mydict[key]
else:
value = mydict[key] = getvalue(key)
Note
Note In Python 2.0 and higher, you can do even better and code this as value = mydict.setdefault(key, getvalue(key)).
There are often times when a lower level library you import throws exceptions that you don’t actually wish to handle. You simply want to pass them up the tree.
An example, urllib throws a socket.error. One very useful way of passing this up the tree is to refine the exception. e.g.
try:
# some code doing authentication for rocon interactions
# it uses some urllib functions which throw socket.errors
catch socket.error as e:
raise InteractionsAuthenticationError("some msg....")
This makes it more readable and provides more flexibility for the user of your library.
Again, what to do with lower level exceptions that are thrown, but you don’t want to handle. How do you make it nice and easy for your library user to know that he’s got to handle these exceptions that are coming his way?
Good Documentation
def foo():
'''
:raises: :exc:`urllib.error.ContentTooShortError` if download content-length error occurred.
'''
Quite often this is all a user sees of your module. e.g. I almost never looking into rospy code, but I’m constantly hitting Ken’s well written rospy api documentation looking for what arg types there are and what exceptions are getting raised.
Re-Raising Exceptions
The following could potentially assist readability in code:
try:
# something
catch SomeLowerLevelException:
raise
However, catching exceptions is said to be very expensive. I do not know if the above is more expensive than just letting it float up, so I am avoiding it for now.
In the python world, hiding, or masking exceptions is considered very bad practice because you take control away from your users. Even if you’re writing a module for an executable (e.g. rapp manager) don’t be surprised if someone links and uses one of your package’s classes, or modules one day...thus becoming your library user (I do this all the time with Ken’s python code, half of which have comments about not recommending its use).
A DONT DO THIS example - hiding known exceptions:
try:
self._publishers['app_list'].publish(rapp_list)
except KeyError:
pass
I just spent 20mins hunting a bug in the rapp manager because this should have been ‘rapp_list’ not ‘app_list’.
The Mother of All Exceptions, Exception should not be used directly, use one from the standard exception heirarchy <https://docs.python.org/2/library/exceptions.html#exception-hierarchy> or customise your own. The key problems here are that you are handling something that may not be what you expect, or you are hiding it from user’s above.
A DONT DO THIS example - hiding the MOTHER OF ALL exceptions:
try:
yourapi.call_me()
except APIKnownError as e:
report(e)
except Exception:
pass
If you don’t know what’s floating up, the worst possible thing to do is hide it.
Another DONT DO THIS example - handling the MOTHER OF ALL exceptions:
try:
foo = opne("file") # misspelled "open"
except Exception as e:
sys.exit("could not open file!")
Here it’s hard to know that it was a programming error that raised a NameError and not related to opening the file, i.e. a IOError.
Ok, so we don’t hide unknown exceptions and since we don’t know what they are, we can’t handle them so you can probably safely conclude that your program is now rogue and likely soon to be fubar no matter what you’re thinking of doing. So I typically transfer the problem to optimising how quickly we can identify what is getting thrown and then fix it.
While in development, for most programs I prefer to let unknown exceptions float up and crash the program. Why?
I’d much rather them jump up and down, come and annoy me, or have a rant on a github issue so the problem immediately comes into focus. Fixing these issues quickly is important because python makes it hard to catch every conceivable problem at compile time.
For a product or a critical program that needs the best chance to exit gracefully (e.g. a powerful manipulator) there are of course other considerations that might be important enough to change this philosophy.