Weird exception handling behavior -- late evaluation in except clause

R

Roy Smith

This is kind of weird (Python 2.7.3):

try:
print "hello"
except foo:
print "foo"

prints "hello". The problem (IMHO) is that apparently the except clause
doesn't get evaluated until after some exception is caught. Which means
it never notices that foo is not defined until it's too late.

This just came up in some code, where I was trying to catch a very rare
exception. When the exception finally happened, I discovered that I had
a typo in the except clause (I had mis-spelled the name of the
exception). So, instead of getting some useful information, I got an
AttributeError :-(

Is this a bug, or intended behavior? It seems to me it would be much
more useful (if slightly more expensive) to evaluate the names of the
exceptions in the expect clause before running the try block.
 
H

Hans Mulder

This is kind of weird (Python 2.7.3):

try:
print "hello"
except foo:
print "foo"

prints "hello". The problem (IMHO) is that apparently the except clause
doesn't get evaluated until after some exception is caught. Which means
it never notices that foo is not defined until it's too late.

This just came up in some code, where I was trying to catch a very rare
exception. When the exception finally happened, I discovered that I had
a typo in the except clause (I had mis-spelled the name of the
exception). So, instead of getting some useful information, I got an
AttributeError :-(

Is this a bug, or intended behavior? It seems to me it would be much
more useful (if slightly more expensive) to evaluate the names of the
exceptions in the expect clause before running the try block.

It's intended behaviour: Python runs your script from top to bottom.
It doesn't peek ahead at except: clauses. Even it an exception is
raised, the exception names are not eveluated all at once. Python
begins by evaluating the first exception name. It the exception at
hand is an instance of that, Python has found a match and the rest
of the names are left unevaluated:
.... 1/0
.... except ZeroDivisionError:
.... print "hello"
.... except 1/0:
.... pass
....
hello

There's probably some lint-like tool that can find this kind of issue.


Hope this helps,

-- HansM
 
T

Terry Reedy

This is kind of weird (Python 2.7.3):

try:
print "hello"
except foo:
print "foo"

prints "hello". The problem (IMHO) is that apparently the except clause
doesn't get evaluated until after some exception is caught. Which means
it never notices that foo is not defined until it's too late.

This just came up in some code, where I was trying to catch a very rare
exception. When the exception finally happened, I discovered that I had
a typo in the except clause (I had mis-spelled the name of the
exception). So, instead of getting some useful information, I got an
AttributeError :-(

You would have the same problem if you spelled the exception name right
but misspelled a name in the corresponding block. This is PyLint,
PyChecker, xxx territory.

Note that proper checking requires execution of imports.

import mymod

try: pass
except mymod.MymogException: pass # whoops
Is this a bug, or intended behavior? It seems to me it would be much
more useful (if slightly more expensive) to evaluate the names of the
exceptions in the expect clause before running the try block.

'try:' is very cheap. Searching ahead any checking names in all the
except clauses would be much more expensive -- and useless after the
first time the code is run after being revised.
 
S

Steven D'Aprano

This is kind of weird (Python 2.7.3):

try:
print "hello"
except foo:
print "foo"

prints "hello". The problem (IMHO) is that apparently the except clause
doesn't get evaluated until after some exception is caught. Which means
it never notices that foo is not defined until it's too late.

This is exactly the same as, well, everything in Python. Nothing is
evaluated until needed.

Consider this piece of legal Python code:

Err = None
if condition(x) > 100:
Err = OneException
elif another_condition(x):
Err = AnotherException
try:
spam(a, b, c)
except Err:
recover()


And consider that spam() may very well set the global Err to yet another
value, or delete it altogether, before raising. How should Python check
that Err is defined to an actual exception ahead of time?

The names of exceptions are no different from any other names in Python.
They're merely names, and they're looked up at runtime. If you want to
boggle/confuse/annoy your friends, shadowing builtins can help:


py> def spam(x):
.... global UnicodeEncodeError
.... UnicodeEncodeError = ZeroDivisionError
.... return 1/x
....
py> try:
.... spam(0)
.... except UnicodeEncodeError:
.... print("Divided by zero")
....
Divided by zero


(By the way, if you ever do this by accident, you can recover with a
simple "del UnicodeEncodeError" to restore access to the builtin.)

As Terry has said, those sort of pre-runtime checks are the
responsibility of pylint or pychecker or equivalent. Python is too
dynamic to allow the sort of compile-time checks you can get from a
Haskell, C or Pascal, so the Python compiler delegates responsibility to
third-party applications. There's no point in making an exceptions for
exceptions.

This just came up in some code, where I was trying to catch a very rare
exception. When the exception finally happened, I discovered that I had
a typo in the except clause (I had mis-spelled the name of the
exception). So, instead of getting some useful information, I got an
AttributeError :-(

One of the nice features of Python 3:

py> try:
.... 1/0
.... except ZeroDividingError:
.... pass
....
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "<stdin>", line 3, in <module>
NameError: name 'ZeroDividingError' is not defined


Very useful for debugging unexpected errors in except clauses. The
downside is that until Python 3.3, there's no way to turn it off when you
intentionally catch one exception and raise another in it's place.

Is this a bug, or intended behavior? It seems to me it would be much
more useful (if slightly more expensive) to evaluate the names of the
exceptions in the expect clause before running the try block.

"Slightly" more expensive? Methinks you are underestimating the level of
dynamism of Python.

py> def pick_an_exception():
.... global x
.... if 'x' in globals():
.... return TypeError
.... x = None
.... return NameError
....
py> for i in range(3):
.... try:
.... print(x + 1)
.... except pick_an_exception() as err:
.... print("Caught", err)
....
Caught name 'x' is not defined
Caught unsupported operand type(s) for +: 'NoneType' and 'int'
Caught unsupported operand type(s) for +: 'NoneType' and 'int'


The except expression can be arbitrarily expensive, with arbitrary side-
effects.
 
C

Chris Angelico

Consider this piece of legal Python code:

Err = None
if condition(x) > 100:
Err = OneException
elif another_condition(x):
Err = AnotherException
try:
spam(a, b, c)
except Err:
recover()

Legal it may be, but are there times when you actually _need_ this
level of dynamism? It strikes me as a pretty weird way of going about
things.

I agree with the point you're making, but this feels like a contrived
example, and I'm curious as to whether it can be uncontrived.

ChrisA
 
S

Steven D'Aprano

Legal it may be, but are there times when you actually _need_ this level
of dynamism? It strikes me as a pretty weird way of going about things.

I agree with the point you're making, but this feels like a contrived
example, and I'm curious as to whether it can be uncontrived.


Yeah, in hindsight it was a pretty crappy example. But this sort of
dynamism really is useful:

def testRaises(exc, func, *args):
try:
result = func(*args)
except exc:
return
raise AssertionError("expected exception but didn't get one")


def wrap(func, exc, default=None):
@functools.wraps(func)
def inner(*args):
try:
return func(*args)
except exc:
return default
return inner
 
C

Chris Angelico

Yeah, in hindsight it was a pretty crappy example. But this sort of
dynamism really is useful:

def testRaises(exc, func, *args):
try:
result = func(*args)
except exc:
return
raise AssertionError("expected exception but didn't get one")


def wrap(func, exc, default=None):
@functools.wraps(func)
def inner(*args):
try:
return func(*args)
except exc:
return default
return inner

Ah, that makes good sense. The 'except' clause takes a parameter, so
it follows logically that you could pass a parameter to something that
wraps an except clause.

ChrisA
 

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,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top