An example of a subtle over-broad try
in Python
Today I wrote some code to winnow a list of users to 'real' users with live home directories that looks roughly like the following:
for uname, hdir in userlist: try: st = os.stat(hdir) if not stat.S_ISDIR(st.st_mode) or \ stat.S_IMODE(st.st_mode) == 0: continue # looks good: print uname except EnvironmentError: # accept missing homedir; might be a # temporarily missing NFS mount, we # can't tell. print uname
This code has a relatively subtle flaw because I've accidentally written an over-broad exception catcher here.
As suggested by the comment, when I wrote this code I intended the
try
block to catch the case where the os.stat
failed. The flaw
here is that print
itself does IO (of course) and so can raise
an IO exception. Since I have the print
inside my try
block, a
print
-raised IO exception will get caught by it too. You might
think that this is harmless because the except
will re-do the
print
and thus presumably immediately have the exception raised
again. This contains two assumptions: that the exception will be
raised again and that if it isn't, the output is in a good state
(as opposed to, say, having written only partial output before an
error happened). Neither are entirely sure things and anyways, we
shouldn't be relying on this sort of thing when it's really easy
to fix. Since both branches of the exception end up at the same
print
, all we have to do is move it outside the try:
block
entirely (the except
case then becomes just 'pass
').
(My view is that print
failing is unusual enough that I'm willing
to have the program die with a stack backtrace, partly because this
is an internal tool. If that's not okay you'd need to put the print
in its own try
block and then do something if it failed, or have
an overall try
block around the entire operation to catch otherwise
unexpected EnvironmentError
exceptions.)
The root cause here is that I wasn't thinking of print
as something
that does IO that can throw exceptions. Basic printing is sufficiently
magical that it feels different and more ordinary, so it's easy to
forget that this is a possibility. It's especially easy to overlook
because it's extremely uncommon for print
to fail in most situations
(although there are exceptions,
especially in Python 3). You can also attribute
this to a failure to minimize what's done inside try
blocks to only
things that absolutely have to be there, as opposed to things that are
just kind of convenient for the flow of code.
As a side note, one of the things that led to this particular case
is that I changed my mind about what should happen when the os.stat()
failed because I realized that failure might have legitimate causes
instead of being a sign of significant problems with an account
that should cause it to be skipped. When I changed my mind I just
did a quick change to what the except
block did instead of totally
revising the overall code, partly because this is a small quick
program instead of a big system.
|
|