30

From the Google Style Guide on lexical scoping:

A nested Python function can refer to variables defined in enclosing functions, but can not assign to them.

Both of these seem to check out at first:

# Reference
def toplevel():
    a = 5
    def nested():
        print(a + 2)
    nested()
    return a
toplevel()
7
Out[]: 5

# Assignment
def toplevel():
    a = 5
    def nested():
        a = 7 # a is still 5, can't modify enclosing scope variable
    nested()
    return a
toplevel()
Out[]: 5

So why, then, does a combination of both reference and assignment in the nested function lead to an exception?

# Reference and assignment
def toplevel():
    a = 5
    def nested():
        print(a + 2)
        a = 7
    nested()
    return a
toplevel()
# UnboundLocalError: local variable 'a' referenced before assignment
Brad Solomon
  • 38,521
  • 31
  • 149
  • 235
  • 1
    Just a note, the `print(a+2);a=7` combination doesn't work, however, the `a=7;print(a+2)` combination works. – DjaouadNM Sep 02 '17 at 22:53
  • The guide in question is, to my understanding, written for Python 2.x, which lacks the `nonlocal` keyword. In 3.x, `nonlocal` can be used to work around the problem described. – Karl Knechtel Sep 09 '22 at 09:28

2 Answers2

37

In first case, you are referring to a nonlocal variable which is ok because there is no local variable called a.

def toplevel():
    a = 5
    def nested():
        print(a + 2) # theres no local variable a so it prints the nonlocal one
    nested()
    return a

In the second case, you create a local variable a which is also fine (local a will be different than the nonlocal one thats why the original a wasn't changed).

def toplevel():
    a = 5 
    def nested():
        a = 7 # create a local variable called a which is different than the nonlocal one
        print(a) # prints 7
    nested()
    print(a) # prints 5
    return a

In the third case, you create a local variable but you have print(a+2) before that and that is why the exception is raised. Because print(a+2) will refer to the local variable a which was created after that line.

def toplevel():
    a = 5
    def nested():
        print(a + 2) # tries to print local variable a but its created after this line so exception is raised
        a = 7
    nested()
    return a
toplevel()

To achieve what you want, you need to use nonlocal a inside your inner function:

def toplevel():
    a = 5
    def nested():
        nonlocal a
        print(a + 2)
        a = 7
    nested()
    return a
Mohd
  • 5,523
  • 7
  • 19
  • 30
  • 1
    In the third case, how does `nested` "know" to search for a local variable rather than use the enclosing function's `a`? I follow your answer otherwise, but not this aspect since python is interpreted line by line. – Brad Solomon Sep 02 '17 at 23:19
  • 2
    Even though its an interpreted language, the whole syntax gets checked before it runs. Have a look at [this](https://stackoverflow.com/a/6889798/7688996) and [this](https://softwareengineering.stackexchange.com/questions/263982/why-is-python-treated-as-a-interpreted-language-when-it-has-a-compiling-stage) – Mohd Sep 02 '17 at 23:30
  • @BradSolomon Python's semantics don't depend on it being interpreted. Even CPython, the canonical, "interpreting" implementation of Python, does not "interpret line by line". It **compiles** the text of the program into bytecode, and that bytecode then runs on the cpython virtual machine (VM). The bytecode is machine code for a particular "CPU" that is the cpython VM. – Kuba hasn't forgotten Monica Dec 08 '21 at 18:33
19

For anyone stumbling across this question, in addition to the accepted answer here, it is answered concisely in the Python docs:

This code:

>>> x = 10
>>> def bar():
...     print(x)
>>> bar()
10

works, but this code:

>>> x = 10
>>> def foo():
...     print(x)
...     x += 1

results in an UnboundLocalError.

This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. Since the last statement in foo assigns a new value to x, the compiler recognizes it as a local variable. Consequently when the earlier print(x) attempts to print the uninitialized local variable and an error results.

In the example above you can access the outer scope variable by declaring it global:

>>> x = 10
>>> def foobar():
...     global x
...     print(x)
...     x += 1
>>> foobar()
10

You can do a similar thing in a nested scope using the nonlocal keyword:

>>> def foo():
...    x = 10
...    def bar():
...        nonlocal x
...        print(x)
...        x += 1
...    bar()
...    print(x)
>>> foo()
10
11
Brad Solomon
  • 38,521
  • 31
  • 149
  • 235