Weird exception handling behavior -- late evaluation in except clause

Steven D'Aprano steve+comp.lang.python at pearwood.info
Sun Dec 2 16:31:51 EST 2012


On Sun, 02 Dec 2012 12:25:22 -0500, Roy Smith wrote:

> 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.



-- 
Steven



More information about the Python-list mailing list