When is bare except: justified?

J

John J. Lee

Bare "except:", with no exception specified, is nasty because it can
easily hide bugs.

There is a case where it seemed useful to me in the past. Now it
seems like a bad idea to me (but I think I might still be confused
about it, hence this post). It's this: When you're processing a input
from a file-like object connected to an external source like a network
connection, disk file etc, you have to expect broken, and often
malicious, data (especially from data that arrived from the internet,
of course). except: seemed to me like a good way of dealing with data
that tripped up tacit assumptions in my code about the data.

First example: I have a method that reads cookie data from an HTTP
response and processes it. If the data is malformed in a way I didn't
anticipate, it seems like it would be nice if the code ignored it
rather than crashing the application using my library, or forcing the
application to halt all processing just because of one little bit of
bad data. Still, if you take the attitude that malformed data should
be explicitly detected by the code (whether by catching an exception
or doing a direct "look before you leap" test), rather than relying on
unexpected exceptions, this technique could be seen as hiding a bug
every time it successfully does its job! So I suppose I should just
let the exception propagate, regard any exceptions that show up as
bugs, and fix them. It is of course often useful to let exceptions do
the work, reducing lines of code and avoiding race conditions by not
explicitly checking things. But, even when faced with malicious
network data, I suppose that's only justified when you understand with
reasonable specificity which exceptions are going to be raised and for
what reason -- and my use of except: definitely does not fall into
that category.

Even more doubtfully, I have a pair of methods .save() and .load().
..save() is straightforward: it writes the object's state to a disk
file. .load() loads new data from a disk file, *appending* to the
data held by the object. .load() is supposed to raise IOError when
something goes wrong (IOError is probably the wrong class here, but
that's another issue). .load() makes sure it only raises that error
by catching everything then re-raising IOError: I do this like so:

try:
self.load_data()
except:
reraise_unmasked_exceptions((IOError,))
raise IOError("invalid file format")

def reraise_unmasked_exceptions(unmasked=()):
# There are a few catch-all except: statements in this module, for
# catching input that's bad in unexpected ways.
# This function re-raises some exceptions we don't want to trap.
if ClientCookie.CLIENTCOOKIE_DEBUG:
raise
unmasked = unmasked + (KeyboardInterrupt, SystemExit)
etype = sys.exc_info()[0]
if issubclass(etype, unmasked):
raise

The network data case uses the same function, but like so:

try:
cookies = self.parse_cookies()
except:
reraise_unmasked_exceptions()
cookies = []

Problems:

0. It's ugly and hard to understand.
1. May mask bugs.
2. Even when bugs are not masked, it won't be obvious what happened
without turning on the debug switch.


Part of the reason it's supposed to only raise that error is that
..revert() is used to to *overwrite* (not append) to the data held by
the objecct, implemented something like this:

def revert(self, filename):
old_state = copy.deepcopy(self.cookies)
self.cookies = {}
try:
self.load(filename)
except IOError:
self.cookies = old_state
raise

But of course the bare except: could equally well be in .revert(), not
..load(). And really, I now think it shouldn't be there at all,
because it can only hide bugs. Right?

When *is* it justified to use except:, then?

I've seen simple uses like this that seem fine (this one from Alex
Martelli, I think):

def isstringlike(x):
try: x+""
except: return False
else: return True


That's fine because x+"" is too simple to have a bug in it. Even
there, though, what happens if KeyboardInterrupt happens just as the
try block is getting executed?

Another reasonable use, in a straightforward debugging hack:

try:
...
except:
# use traceback module here to print out some detail of the exception
...
raise


And:

try:
use_dodgy_plugin_code()
except:
print "your plugin is buggy"


Anywhere else?


John
 
D

Dave Brueck

When *is* it justified to use except:, then?
I've seen simple uses like this that seem fine (this one from Alex
Martelli, I think):
[snip: example where code is probably too simple to contain a bug]
Another reasonable use, in a straightforward debugging hack:

try:
...
except:
# use traceback module here to print out some detail of the exception
...
raise


And:

try:
use_dodgy_plugin_code()
except:
print "your plugin is buggy"

The rule of thumb I use is that a bare except is bad if it hides the fact that
an exception occurred. So, for example, using a bare except in code like this
is okay:

for worker in workers:
try:
worker.process()
except:
LogException()

It's considered "okay" because even though the code prevents the exception from
propagating, it doesn't lose record of the fact that something went wrong.

We use these catch-all bare excepts in production code all the time to contain
failures to as small an area as possible. One particular example where this is
critical is in one application where we have a larger framework that also runs
some custom code for each customer (like your plugin example). Although it's
code we develop and own, we still have catch-alls around their execution so
that the failure of one doesn't interrupt service of the others.

There are actually quite a few cases like this for us where the reasons for
partial failure are too many to enumerate, and we'd prefer to keep executing
because the cause of the failure may go away over time and/or partial failure
is preferrable to total failure.

-Dave
 
P

Peter Hansen

John J. Lee said:
Bare "except:", with no exception specified, is nasty because it can
easily hide bugs.
[snip remainder of excellent comments]

I believe the only case where we permit "bare" except statements is when
there's a clear intention that we want higher level code to be immune to
problems in the lower level code, primarily because the higher level code
makes up part of a long-running process.

In a nutshell, if we have one of our multi-threaded embedded applications,
it's more important that the program as a whole keep running than it is
to expose potentially minor problems by crashing the entire system with a
nasty traceback message exposed to the user.

Since most of the lower level code has decent unit tests, and we have
acceptance tests covering the combination of it plus higher level code,
we tend to accept that the basic functionality of the system is already
guaranteed, and therefore any unhandled exceptions would represent things
which it's not worth shutting down over. We do log them in a log file,
but we then ignore the error and continue running, or restart the thread,
as appropriate.

In all other cases, it's an unwritten rule that bare except is a sign of
poorly written code.

(I may have overlooked a special case or two where we would make an
exception, no pun intended... not sure.)

-Peter
 
P

Peter Hansen

The rule of thumb I use is that a bare except is bad if it hides the fact that
an exception occurred. So, for example, using a bare except in code like this
is okay:

for worker in workers:
try:
worker.process()
except:
LogException()

Ah, the sheer elegant beauty of that viewpoint. Thanks, Dave! It clearly
covers all the use cases we have, with a much, much smaller rationalization
than I've tried to use.

-Peter
 
J

John J. Lee

Dave Brueck said:
When *is* it justified to use except:, then?
[...]
The rule of thumb I use is that a bare except is bad if it hides the fact that
an exception occurred. So, for example, using a bare except in code like this
is okay:

for worker in workers:
try:
worker.process()
except:
LogException()

It's considered "okay" because even though the code prevents the exception from
propagating, it doesn't lose record of the fact that something went wrong.
[...]

Now that you say that, it's obvious :) In fact, IIRC just the other
day I fixed code to work that way in the same package I discussed in
my post:

try:
__import__(module_name)
except ImportError:
+ traceback.print_exc()
sys.exit("Import of test module failed -- Couldn't find tests?")

(this is in a hack to import all tests from a bunch of test_foo.py files)


I guess I can keep the bare except:s I discussed in the first part of
my post after all, but just make sure I emit a warning (with the
warnings module in 2.3, I suppose).

Thanks!


John
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,766
Messages
2,569,569
Members
45,042
Latest member
icassiem

Latest Threads

Top