What does this print, #1

I feel like I spend a fair amount of time investigating corner cases of the Python language; Python is relatively well-documented, but the documentation falls very short of a full language specification, so often the only recourse is to write a test case and run against CPython as a reference.  Sometimes the answers are pretty counter-intuitive, like this one:

X = 0
Y = 0
def wrapper():
    X = 1
    Y = 1
    class C(object):
        print X, Y # <- what happens at this line?
        X = 2
wrapper()

I’ll let you run it for yourself to not spoil the surprise.

3 responses to “What does this print, #1”

  1. WTF? 0 and 1?
    What the heck is going on there?
    Does the X = 2 lines “compilation” somehow copies the X name from the global scope inside the C class, before X=2 is evaluated proper?
    If I add Y = 2 after X=2, then its print 0 0 ….
    I just cannot grok the scoping madness there
    Tell us more!

    Like

  2. Do you have any idea of the logic behind this (i.e. couldn’t this be a bug)? I was expecting 1, 1.

    This is also pretty confusing:

    X = 0
    Y = 0
    def wrapper():
    X = 1
    Y = 1
    class C(object):
    print X, Y # <- what happens at this line?
    X = 2
    Y = 2
    wrapper()

    Like

  3. Nope doesn’t copy X from the global scope — you can verify this by adding a ‘print locals()’ after the existing print line, or by surrounding the ‘X = 2’ with an ‘if 0:’, and checking C.__dict__ after the class is created.

    As far as I can tell this isn’t a bug, since both CPython and PyPy produce this result, though I have no idea who would rely on this behavior.

    Here’s the best reference that I could find:
    http://www.gossamer-threads.com/lists/python/dev/254461

    It’s an old thread from 2002 that seems to allude to this behavior being around for backwards compatibility. My reading is that at some point *all* lookups worked this way, ie this code would have printed “0 0”. Then at some point they added nested functions and changed the way that function scoping works, but didn’t apply the same change to class scoping.

    The technical details are that there are a number of different opcodes that Python can use to look up names; in a function scope locals are looked up with LOAD_FAST, but in a classdef they are looked up with LOAD_NAME, which does not check any parent scopes and just skips to the global scope. Non-locals in classdefs are looked up with either LOAD_NAME or LOAD_DEREF, the latter of which will check enclosing scopes.

    Not something you run into every day but something you have to get right as an implementor!

    Like

Leave a comment