Search code examples
pythonpython-3.xpython-internals

Why doesn't co_varnames return list of all the variable names?


Why the result of below snippets are different?

def Foo():
  i = 0
  def Bar():
    nonlocal i
    i = 1
  return Bar()

print(Foo.__code__.co_varnames)

# it will print: ('Bar',)
def Foo():
  i = 0
  def Bar():
    i = 1
  return Bar()

print(Foo.__code__.co_varnames)

# it will print: ('i', 'Bar',)

As you see, the results are different and I don't know why they are different.


Solution

  • Ok, this is a bit subtle. While co_varnames should give you all local variables, in this case,i is actually a free variable in Bar, so it is in Bar's closure.

    Notice what happens when we dissasemble the bytecode:

    In [2]: def Foo():
       ...:   i = 0
       ...:   def Bar():
       ...:     nonlocal i
       ...:     i = 1
       ...:   return Bar()
       ...:
    
    In [3]: Foo.__code__.co_varnames
    Out[3]: ('Bar',)
    
    
    In [4]: import dis
    
    In [5]: dis.dis(Foo)
      2           0 LOAD_CONST               1 (0)
                  2 STORE_DEREF              0 (i)
    
      3           4 LOAD_CLOSURE             0 (i)
                  6 BUILD_TUPLE              1
                  8 LOAD_CONST               2 (<code object Bar at 0x101072be0, file "<ipython-input-4-a3a062461e32>", line 3>)
                 10 LOAD_CONST               3 ('Foo.<locals>.Bar')
                 12 MAKE_FUNCTION            8 (closure)
                 14 STORE_FAST               0 (Bar)
    
      6          16 LOAD_FAST                0 (Bar)
                 18 CALL_FUNCTION            0
                 20 RETURN_VALUE
    
    Disassembly of <code object Bar at 0x101072be0, file "<ipython-input-4-a3a062461e32>", line 3>:
      5           0 LOAD_CONST               1 (1)
                  2 STORE_DEREF              0 (i)
                  4 LOAD_CONST               0 (None)
                  6 RETURN_VALUE
    

    Notice, the opcode is STORE_DEREF not STORE_FAST. So, consider:

    In [9]: def Foo():
       ...:   i = 0
       ...:   def Bar():
       ...:     nonlocal i
       ...:     i = 1
       ...:   return Bar
       ...:
    
    In [10]: bar = Foo()
    
    In [11]: bar.__closure__
    Out[11]: (<cell at 0x1040a7940: int object at 0x100980bc0>,)
    

    Since it is referenced in a nested function, this will be available in co_cellvars:

    In [12]: foo_code = Foo.__code__
    
    In [13]: foo_code.co_cellvars
    Out[13]: ('i',)
    

    This is explained in the data-model documentation for internal types:

    co_varnames is a tuple containing the names of the local variables (starting with the argument names); co_cellvars is a tuple containing the names of local variables that are referenced by nested functions; co_freevars is a tuple containing the names of free variables...

    So to get all local variables you need:

    In [16]: Foo.__code__.co_varnames + Foo.__code__.co_cellvars
    Out[16]: ('Bar', 'i')