Emma Emma - 2 months ago 12
Python Question

Unbound name showing up in stack frame using inspect module

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.

Answer

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