Thursday, April 18, 2013

Python 3 Language Gotcha - and a short reminisce

There's a lot of Python nostalgia going around today, from Brett Cannon's 10 year anniversary of becoming a core developer, to Guido reminding us that he came to the USA 18 years ago.  Despite my stolen time machine keys, I don't want to dwell in the past, except to say that I echo much of what Brett says.  I had no idea how life changing it would be -- on both a personal and professional level -- when Roger Masse and I met Guido at NIST at the first Python workshop back in November 1994.  The lyric goes: what a long strange trip it's been, and that's for sure.  There were about 20 people at that first workshop, and 2500 at Pycon 2013.

And Python continues to hold little surprises.  Just today, I solved a bug in an Ubuntu package that's been perplexing us for weeks.  I'd looked at the code dozens of times and saw nothing wrong.  I even knew about the underlying corner of the language, but didn't put them together until just now.  Here's a boiled down example, see if you can spot the bug!

import sys

def bar(i):
    if i == 1:
        raise KeyError(1)
    if i == 2:
        raise ValueError(2)


def bad():
    e = None
    try:
        bar(int(sys.argv[1]))
    except KeyError as e:
        print('ke')
    except ValueError as e:
        print('ve')
    print(e)

bad()


Here's a hint: this works under Python 2, but gives you an UnboundLocalError on the `e` variable under Python 3.

Why?

The reason is that in Python 3, the targets of except clauses are `del`d from the current namespace after the try...except clause executes.  This is to prevent circular references that occur when the exception is bound to the target.  What is surprising and non-obvious is that the name is deleted from the namespace even if it was bound to a variable before the exception handler!  So really, setting `e = None` did nothing useful!

Python 2 doesn't have this behavior, so in some sense it's less surprising, but at the expense of creating circular references.

The solution is simple.  Just use a different name to capture and use the exception outside of the try...except clause.  Here's a fixed example:

def good():
    exception = None
    try:
        bar(int(sys.argv[1]))
    except KeyError as e:
        exception = e
        print('ke')
    except ValueError as e:
        exception = e
        print('ve')
    print(exception)


So even after almost 20 years of hacking Python, you can still experience the thrill of discovering something new.

3 comments:

  1. Nick reminds me that there's no circular reference in Python 2. This happens in Python 3 because of the addition of __traceback__ as part of exception chaining.

    ReplyDelete
  2. http://bugs.python.org/issue17792

    ReplyDelete
  3. the world is small, and more projects had this bug:

    https://github.com/halst/schema/pull/6

    however, i think the fixed code is much better if you actually see the “as” keyword like it is: a *scoped* assignment. you can also see that in my comment in the pull request: i only understood the code after trying to port it to python broke it!

    ReplyDelete