After reading Chris McDonough’s What Not To Do When Writing Python Software, it occurred to me that many people don’t actually know how to properly re-raise exceptions. So a little mini-tutorial for Python programmers, about exceptions…
First, this is bad:
try:
some_code()
except:
revert_stuff()
raise Exception("some_code failed!")
It is bad because all the information about how some_code() failed is lost. The traceback, the error message itself. Maybe it was an expected error, maybe it wasn’t.
Here’s a modest improvement (but still not very good):
try:
some_code()
except:
import traceback
traceback.print_exc()
revert_stuff()
raise Exception("some_code failed!")
traceback.print_exc() prints the original traceback to stderr. Sometimes that’s the best you can do, because you really want to recover from an unexpected error. But if you aren’t recovering, this is what you should do:
try:
some_code()
except:
revert_stuff()
raise
Using raise with no arguments re-raises the last exception. Sometimes people give a blank never use “except:“ statement, but this particular form (except: + raise) is okay.
There’s another form of raise that not many people know about, but can also be handy. Like raise with no arguments, it can be used to keep the traceback:
try:
some_code()
except:
import sys
exc_info = sys.exc_info()
maybe_raise(exc_info)
def maybe_raise(exc_info):
if for some reason this seems like it should be raised:
raise exc_info[0], exc_info[1], exc_info[2]
This can be handy if you need to handle the exception in some different part of the code from where the exception happened. But usually it’s not that handy; it’s an obscure feature for a reason.
Another case when people often clobber the traceback is when they want to add information to it, e.g.:
for lineno, line in enumerate(file):
try:
process_line(line)
except Exception, exc:
raise Exception("Error in line %s: %s" % (lineno, exc))
You keep the error message here, but lose the traceback. There’s a couple ways to keep that traceback. One I sometimes use is to retain the exception, but change the message:
except Exception, exc:
args = exc.args
if not args:
arg0 = ''
else:
arg0 = args[0]
arg0 += ' at line %s' % lineno
exc.args = arg0 + args[1:]
raise
It’s a little awkward. Technically
(though it’s deprecated) you can raise
anything as an exception. If you
use
except
Exception:
you won’t catch things like string
exceptions or other weird types. It’s
up to you to decide if you care about these
cases; I generally ignore them. It’s
also possible that an exception won’t
have
.args, or the string message for the exception
won’t be derived from those arguments,
or that it will be formatted in a funny
way
(KeyError
formats its message differently, for
instance). So this isn’t foolproof. To
be a bit more robust, you can get the
exception like this:
except:
exc_class, exc, tb = sys.exc_info()
exc_class will be a string, if someone does something like raise "not found". There’s a reason why that style is deprecated. Anyway, if you really want to mess around with things, you can then do:
new_exc = Exception("Error in line %s: %s"
% (lineno, exc or exc_class))
raise new_exc.__class__, new_exc, tb
The confusing part is that you’ve changed the exception class around, but you have at least kept the traceback intact. It can look a little odd to see raise ValueError(...) in the traceback, and Exception in the error message.
Anyway, a quick summary of proper ways to re-raise exceptions in Python. May your tracebacks prosper!
Update: Kumar notes the problem of errors in your error handler. Things get more long winded, but here’s the simplest way I know of to deal with that:
try:
code()
except:
exc_info = sys.exc_info()
try:
revert_stuff()
except:
# If this happens, it clobbers exc_info, which is why we had
# to save it above
import traceback
print >> sys.stderr, "Error in revert_stuff():"
traceback.print_exc()
raise exc_info[0], exc_info[1], exc_info[2]
No related posts.
Scratch that, stupid mistake on my part. Still might be worth mentioning try/finally while we’re talking about this:
when cleanup stuff needs to be executed whether there is an error not.
No!
finally:
blocks are always run, success or failure. You don’t want torevert_stuff()
always, you only want to do it in the case of an exception.except:/raise
does that.Heh, yup caught that right after I posted it. :)
Thanks for posting, Ian. Like many of us, I had to learn all this the hard way ;)
Along the lines of your last example,
it is very tempting to do this:
but this introduces a very subtle bug where if the exception constructor accepts a non-standard number of args then it will give you a very confusing error with a traceback leading to the wrong place. This bit me once and I had to update a lot of code (!) so I thought it was worth pointing out. Creating a new exception like you did is the safe approach.
UnicodeEncode/DecodeError
is probably the worse — it has a particularly complicated constructor. And it’s pretty common to boot. And it breaks my recipe about modifyingexc.args
too — the message there is actually inexc.args[-1]
, notexc.args[0]
(or the closest thing to a single message). Adding toexc.args
also doesn’t work in that case, as any extra items are just ignored.http://docs.python.org/lib/module-sys.html says:
This passage has always scared me a little bit, so I’ve made a point to either
del exc_info
(exc_info
as you’ve used in your examples) or just not assign it to a local variable. So above you couldmaybe_raise(sys.exc_info())
to avoid creating the cycle, or just havemaybe_raise
callsys.exc_info()
itself. I think.For trivial examples this isn’t a deal-breaker, but I’d imagine this could cause a lot of extra memory to be allocated if the exception handling is frequently used. (Of course, that’s not very exceptional, is it?)
(Also, the Markdown code you’re using doesn’t seem to get “<http://whatever>” right, nor “> foo” for a blockquote? And it seems like I can’t get a < in a code section? Or is that just a broken preview?)
I’m fond of this little function for those cases when I don’t want to re-raise but I still want to know what happened:
Paul, You can achieve the same with the following inline code:
However, what I like to do in such a scenario is is to conditionally propagate the exception, something like this (for a db operation):
The advantage is that if the db operation fails (say a constraint violation), then it does a rollback (while making sure to not stomp on the original exception), but when the operation succeeds, any exception in commit is taken seriously and so is propagated.
Ooops… sorry, the logging call has a typo, it should be:
or simply:
Adding information to exceptions has been very nicely implemented in Java – you can pass the original exception to your exception’s constructor and get a chained stack trace. Flame away, but I think that Java beats Python in this area .
There may be some very useful information in the above blog post, but I only skimmed it and walked away with the suspicion that it’s all bullshit. Here’s why (in the hope that you’ll fix this, Ian): In none of the examples the except clause mentions a specific exception. (It wouldn’t have to be a real exception, FooException would do for the purposes of an explanation.) I.e. all of the examples are doing it WRONG.
Of course you’re throwing away information when you
try
something, and if it fails, no matter how exactly it fails, you always react in exactly the same way and raise an exception of your own that pretends to tell users/developers what actually happened. The information that is now hopelessly lost is whether that error handling was actually appropriate, whether theexcept
clause actually dealt with the original exception that had come up.On the other hand, it is entirely plausible that I know for sure that when accessing my
user_dict
raises aKeyError
(not any error, mind you!), it is appropriate to react by swallowing that error and raising my ownUserDoesntExistException
. It is entirely plausible that, in this case, there’s no benefit to be gained from re-raising thatKeyError
and bothering the users of my library with implementation details that they don’t need to care about.However, it’s important to make the relevant
except
clause specific toKeyError
— it would be a mistake to lazily writeexcept:
instead ofexcept KeyError:
.Adding information to exceptions has been very nicely implemented in Java – you can pass the original exception to your exception’s constructor and get a chained stack trace. Flame away, but I think that Java beats Python in this area .
That feature showed up in Java in the 1.4 release, I believe. It wasn’t there originally; I know, because I had to implement it myself before Java 1.4 was out. It’s also not a feature of the language, per se; it’s a feature of how the exception library classes are written. Seems to me that one could add the capability of nested, or wrapped, exceptions to Python easily enough.
It is a useful solution to the problem, because you get tracebacks for everything in the chain of nested exceptions. A little verbose at times, but a useful compromise.
nex: when you respond to something after only skimming the article, you run the risk of looking dumb. Like now. When you re-raise an exception, you are looking for unexpected exceptions. You don’t know what the exception is. And you don’t have to swallow it, that’s exactly what I’m showing in this article.
re: Java — I believe (maybe only in py3k…?) that the traceback object is being added as an instance variable in the exception, which will probably make this all easier, and similar to in Java.
Cascading tracebacks is an interesting idea. I wonder if it would be annoying or useful if you actually saw each traceback, terminating at the spots you had caught/re-raised. In the current one-traceback world of Python, life is simpler, but in those rare cases where an exception is raised in your exception-handling code then you might as well be writing PHP4: you have to retrace your steps, uncomment stuff, etc, before you can figure out the root cause. Of course one could hand-code cascading tracebacks with the
traceback.print_exc()
recipe above but if there was a way to make python do this automatically that might be useful.Thanks for the quick reply, Ian! Now I know why my suspicion was dumb. I got confused when you mentioned recovering from unexpected errors. Is there actually a way to recover from the error (then it can’t be unexpected, as we did anticipate it, and we should have explicitly declared which error we’re trying to recover from), or is the program going to abort with failure anyways? I don’t think the latter case is what “What Not To Do When Writing Python Software” was talking about, and when people use
except:
in that latter case, they’re most often doing it wrong.Then again, this is exactly what you’re tackling here, teaching people how to do it right. So, all is well; but I think my confusion is excusable, since re-raising actually doesn’t necessarily imply that you’re dealing with a catastrophic failure and are going to abort without an attempt of recovery. That’s just one case. (Another case, for example, would be guarding against a specific error, trying to handle it, and having the error handling fail. I can think of more examples, but I’m sure you can do so as well.)
Re: Adding information to exceptions/Java
Would the [tb_next](http://docs.python.org/ref/types.html#traceback “Python Language Reference”) attribute of traceback objects serve this purpose?
Ross
tb_next
can be used to walk through the frames of the traceback; the traceback object looks kind of like a linked list of calling frames.Re: Traceback as an instance variable in exceptions
Traceback as an instance variable is a good start, combined with an optional ’cause exception’ instance variable would practically yield the Java solution. Well, this and support in the ‘language culture’ – making people actually use it :-)
(accidental double post, sorry)
Ian, was just attempting to use the ‘add message’ recipe, and found it had a type error:
the second to last line should be
Here’s a slightly more worked-out example, maybe a little over-functional in style, but it’s flexible. With the
prepend_exc_str
function (defined later), you can do:and get the result:
Here’s the code, ready to change for other forms of message modification:
Very helpful discussion. A Late comment:
The issue of re-raising base Exception instead of the original exception to avoid non-standard exception class constructors (like e.g. UnicodeEncodeError) could be addressed by dynamically creating a derived exception class:
As a derived class, the re-raised exception would even be caught later on if a dedicated exception handler for the original exception existed somewhere further up the call stack.
In action:
Correction: issubclass() raises an error itself when fed with a non-class first argument, so reraise() should be:
Well, in simple cases, when you don’t need to change message and just do re-raise, you can use the simple
raise exc_info[1], None, exc_info[2]
Your
raise exc_info[0], exc_info[1], exc_info[2]
is incorrect, at least, in Python 2.5!