0

I recently ran into a bug that was quite difficult to track down. I had accidentally re-used a class name as a variable (see code below), so when I tried to call the class I (understandably) got an error. The reason it was so hard to track down is that my debugger (Wing IDE 5.1.10) would execute the line successfully in the debug probe, but when I tried to run the same line in the interpreter it errored out. On further investigation, I found that when I examined the frame data using the inspect module, the name was still shown as a global variable bound to my class. So, I was mystified at receiving a UnboundLocalError on a name that was clearly defined and bound in my frame.

This reproduces the issue:

import inspect

class MyClass(object):
    def __init__(self):
        print "MyClass init() method called successfully"

def newscope():

    #MyClass is not in the current frame's locals:
    assert 'MyClass' not in inspect.currentframe().f_locals.keys()

    #MyClass is in the current frame's globals and can be called successfully:
    class_object = inspect.currentframe().f_globals['MyClass']
    print class_object
    class_object()

    #But, calling MyClass by name results in UnboundLocalError: local
    #variable 'MyClass' referenced before assignment:
    print MyClass

    #Strangely, if at this point I go into the debug probe and run the same
    #line (print MyClass) it executes successfully, printing 
    #"<class '__main__.MyClass'>"

    #Re-assigning the name MyClass is what causes the UnboundLocalError:
    MyClass = 5

if __name__ == '__main__':
    newscope()

Results:

<class '__main__.MyClass'>
MyClass init() method called successfully
Traceback (most recent call last):
  Python Shell, prompt 1, line 29
  Python Shell, prompt 1, line 19
UnboundLocalError: local variable 'MyClass' referenced before assignment

Again, I understand why I am getting the UnboundLocalError. What I don't understand is why the inspect module is still showing the name as being bound to the class object when clearly that isn't the case. Am I missing something, or is this a bug in the inspect module?

I'm running python 2.7.11.

Emma
  • 1,287
  • 2
  • 17
  • 22
  • Can you add parentheses around the print arguments so that we can compare easily py3 and py2 behaviour ? – hl037_ Sep 12 '16 at 18:31
  • adding `global MyClass` permits to make it work, but it's strange indeed, I'm looking in the documentation if using a name before it's declaration should hide the global one – hl037_ Sep 12 '16 at 18:32
  • Then everything is as expected, look at my answer for the full explanation – hl037_ Sep 12 '16 at 19:25

2 Answers2

1

First, about the exception, I think your IDE doesn't respect the python specs :

A scope defines the visibility of a name within a block. If a local variable is defined in a block, its scope includes that block.

[...]

If a name is bound in a block, it is a local variable of that block. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.

[...]

When a name is not found at all, a NameError exception is raised. If the name refers to a local variable that has not been bound, a UnboundLocalError exception is raised. UnboundLocalError is a subclass of NameError.

https://docs.python.org/2.7/reference/executionmodel.html#naming-and-binding

Thus, I understand the whole block is parsed, it finds your variable, and it is added to the local scope, but before its assignment, it's considered as a free variable

EDIT

About inspect, I think it lists the bound variables in the local namespace, thus, you don't see your variable. it's pretty logical : what value would you give to the key 'MyClass' if it is not bound yet ?

Actually, you should use the inspect.currentframe().f_code.co_varnames to get what you want ;)

import inspect
from pprint import pprint

class MyClass(object):
        def __init__(self):
                print("MyClass init() method called successfully")

def newscope():
        pprint(inspect.currentframe().f_code.co_varnames)
        print("----------")
        pprint(inspect.currentframe().f_locals)
        print("----------")
        pprint(inspect.currentframe().f_globals)
        print("----------")
        try:
                pprint(MyClass)
        except Exception as e:
                print(e)
        MyClass = 5
        pprint(inspect.currentframe().f_locals)
        print("----------")
        pprint(inspect.currentframe().f_globals)
        print("----------")

if __name__ == '__main__':
        newscope()

and you get :

('MyClass', 'e')
----------
{}
----------
{'MyClass': <class '__main__.MyClass'>,
 '__builtins__': <module 'builtins' (built-in)>,
 '__cached__': None,
 '__doc__': None,
 '__file__': 'test.py',
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f2fa3901160>,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'inspect': <module 'inspect' from '/usr/lib/python3.5/inspect.py'>,
 'newscope': <function newscope at 0x7f2fa39b8f28>,
 'pprint': <function pprint at 0x7f2fa1fe66a8>}
----------
local variable 'MyClass' referenced before assignment
{'MyClass': 5}
----------
{'MyClass': <class '__main__.MyClass'>,
 '__builtins__': <module 'builtins' (built-in)>,
 '__cached__': None,
 '__doc__': None,
 '__file__': 'test.py',
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f2fa3901160>,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'inspect': <module 'inspect' from '/usr/lib/python3.5/inspect.py'>,
 'newscope': <function newscope at 0x7f2fa39b8f28>,
 'pprint': <function pprint at 0x7f2fa1fe66a8>}
----------

Remove your variable

import inspect
from pprint import pprint
class MyClass(object):
        def __init__(self):
                print("MyClass init() method called successfully")

def newscope():
        pprint(inspect.currentframe().f_code.co_varnames)
        print("----------")
        pprint(inspect.currentframe().f_locals)
        print("----------")
        pprint(inspect.currentframe().f_globals)
        print("----------")
        try:
                pprint(MyClass)
        except Exception as e:
                print(e)
        # MyClass = 5
        pprint(inspect.currentframe().f_locals)
        print("----------")
        pprint(inspect.currentframe().f_globals)
        print("----------")

if __name__ == '__main__':
        newscope()

and you get :

('e',)
----------
{}
----------
{'MyClass': <class '__main__.MyClass'>,
 '__builtins__': <module 'builtins' (built-in)>,
 '__cached__': None,
 '__doc__': None,
 '__file__': 'test.py',
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fc6d3fcb160>,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'inspect': <module 'inspect' from '/usr/lib/python3.5/inspect.py'>,
 'newscope': <function newscope at 0x7fc6d4082f28>,
 'pprint': <function pprint at 0x7fc6d26b06a8>}
----------
<class '__main__.MyClass'>
{}
----------
{'MyClass': <class '__main__.MyClass'>,
 '__builtins__': <module 'builtins' (built-in)>,
 '__cached__': None,
 '__doc__': None,
 '__file__': 'test.py',
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fc6d3fcb160>,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'inspect': <module 'inspect' from '/usr/lib/python3.5/inspect.py'>,
 'newscope': <function newscope at 0x7fc6d4082f28>,
 'pprint': <function pprint at 0x7fc6d26b06a8>}
----------
hl037_
  • 3,520
  • 1
  • 27
  • 58
  • Ah, code object. That makes sense. I thought that f_builtins, f_locals and f_globals was the whole story. Thanks for enlightening me! – Emma Sep 12 '16 at 20:54
1

If a value is assigned to a variable within a function, that variable becomes a local variable within that function.

That variable is treated as local from the moment the function is created, i.e. before it is called for the first time. Python actually optimizes access to local variables and does not make a lookup into the locals() dictionary, but "knows" exactly where to find each local variable (see this answer about performance within a function).

So, the fact that this assignemt is done at the end of the function does not make a difference. Within your function newscope, variable MyClass is a local variable. Assigning to the MyClass variable after using it is actually what causes the UnboundLocalError in this example.

Take a simpler example:

a = 4

def global_a_example():
    print a   # a is a global variable: prints 4

def local_a_example():
    a = 5
    print a   # a is a local variable: prints 5

def unbound_local_a_example():
    print a   # a is a local variable, but not initialized: raises UnboundLocalError
    a = 5

EDIT: explanation why it looks like the variable is bound

Note that the unbound locals do not end up in the locals dict. That is not because they are not locals. It is because they are unbound. See the following example:

a = 1
b = 2

def f():
    assert 'a' in globals()    # of course
    assert 'a' not in locals() # local 'a' has not been initialized and has no value
    assert 'a' in f.__code__.co_varnames # 'a' is local nevertheless!
    assert 'b' not in f.__code__.co_varnames # 'b' is not local...

    # a != 1 test would raise and exception here because 'a' is local and uninitialized

    a = 10                     # initialize local 'a' (and store it in locals)

    assert 'a' in globals()    # it is still in globals, yes
    assert 'a' in locals()     # it is also in locals

    assert globals()['a'] == 1 # global 'a' has value 1
    assert locals()['a'] == 2  # local 'a' has value 2
    assert a == 10             # a is local 'a'!
    assert b == 2              # b is global 'b'

# but you don't even have to call f()!!!
# 'a' is already defined to be a local variable in f, see:

print f.__code__.co_varnames # prints ('a',)

So, 'a' is not bound until written to. It is a key in the globals dict, but that is irrelevant. It is not used from that dict, because it is defined to be local.

Community
  • 1
  • 1
zvone
  • 18,045
  • 3
  • 49
  • 77
  • Thanks for the explanation, the source of the UnboundLocalError is indeed a subtle point, and it took me a bit of thinking before I realized why that happened. But, my question is actually not about the UnboundLocalError (which should be expected, as you pointed out), but about the fact that this unbound variable is showing up as a bound variable when I examine the stack. – Emma Sep 12 '16 at 20:19
  • @Emma I see. I added the additional explanation to my answer. The variable is not really bound, you are just interpreting the inspection results incorrectly ;) – zvone Sep 12 '16 at 20:43