pythonpython-3.xscopeglobalfree-variable

Free variables treated as globals in Python?


In the Execution Model section of the Python 3.7 reference manual I read the following statement:

The global statement has the same scope as a name binding operation in the same block. If the nearest enclosing scope for a free variable contains a global statement, the free variable is treated as a global.

So I typed the following code into the Python Interpreter:

x =0
def func1():
    global x
    def func2():
        x = 1
    func2()

After calling func1() I would have expected the value of x in the global scope to change to 1.

What did I get wrong?


Solution

  • x = 1 in func2 is not a free variable. It's just another local; you are binding to the name and names bound to are, by default, locals unless you tell Python otherwise.

    From the same Execution model documentation:

    If a name is bound in a block, it is a local variable of that block, unless declared as nonlocal or global. [...] If a variable is used in a code block but not defined there, it is a free variable.

    (Bold emphasis mine)

    You bound the name in the block with x = 1, so it is a local variable in that block, and can't be a free variable. So section you found doesn't apply, because that would only apply to free variables:

    If the nearest enclosing scope for a free variable contains a global statement, the free variable is treated as a global.

    You should not bind x in func2(), because only names that are not binding in the scope are free variables.

    So this works:

    >>> def func1():
    ...     global x
    ...     x = 1
    ...     def func2():
    ...         print(x)  # x is a free variable here
    ...     func2()
    ...
    >>> func1()
    1
    >>> x
    1
    

    x in func2 is now a free variable; it is not defined in the scope of func2, so picks it up from the parent scope. The parent scope here is func1, but x is marked a global there so when reading x for the print() function the global value is used.

    Contrast this with x not being marked as a global in func1:

    >>> def func1():
    ...     x = 1
    ...     def func2():
    ...         print(x)  # x is free variable here, now referring to x in func1
    ...     func2()
    ...
    >>> x = 42
    >>> func1()
    1
    

    Here the global name x is set to 42, but this doesn't affect what is printed. x in func2 is a free variable, but the parent scope func1 only has x as a local name.

    It becomes all the more interesting when you add a new outer-most scope where x is still local:

    >>> def outerfunc():
    ...     x = 0   # x is a local
    ...     def func1():
    ...         global x   # x is global in this scope and onwards
    ...         def func2():
    ...             print('func2:', x)  # x is a free variable
    ...         func2()
    ...     print('outerfunc:', x)
    ...     func1()
    ...
    >>> x = 42
    >>> outerfunc()
    outerfunc: 0
    func2: 42
    >>> x = 81
    >>> outerfunc()
    outerfunc: 0
    func2: 81
    

    x in outerfunc is bound, so not a free variable. It is therefore a local in that scope. However, in func1, the global x declaration marks x as a global in the nested scrope. In func2 x is a free variable, and by the statement that you found, it is treated as a global.