2

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.

This specification can be seen here:

def toplevel():
    a = 5
    def nested():
        # Tries to print local variable `a`, but `a` is created locally after,
        # so `a` is referenced before assignment.  You would need `nonlocal a`
        print(a + 2)
        a = 7
    nested()
    return a
toplevel()
# UnboundLocalError: local variable 'a' referenced before assignment

Reversing the order of the two statements in nested gets rid of this issue:

def toplevel():
    a = 5
    def nested():
        # Two statements' order reversed, `a` is now locally assigned and can
        # be referenced
        a = 7
        print(a + 2)
    nested()
    return a
toplevel()

My question is, what is it about Python's implementation that tells the first function that a will be declared locally (after the print statement)? My understanding is that Python is effectively interpreted line by line. So, shouldn't it default to looking for a nonlocal a at that point in the code?

To elaborate, if I was to use just reference (no assignment),

def toplevel():
    a = 5
    def nested():
        print(a + 2)
    nested()
    return a
toplevel()

somehow the print statement knows to reference the nonlocal a defined in the enclosing function. But if I assign to a local a after that line, the function is almost too smart for its own good.

3 Answers 3

1

My understanding is that Python is effectively interpreted line by line.

That's not the right mental model.

The body of the entire function is analysed to determine which names refer to local variables and which don't.

To simplify your example, the following also gives UnboundLocalError:

def func():
  print(a)
  a = 2

func()

Here, func() compiles to the following bytecodes:

  2           0 LOAD_FAST                0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE

  3           5 LOAD_CONST               1 (2)
              8 STORE_FAST               0 (a)
             11 LOAD_CONST               0 (None)
             14 RETURN_VALUE

Compare this with

def gunc():
  print(a)

which compiles to

  2           0 LOAD_GLOBAL              0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE

Observe how the absence of assignment to a turns the reference from a local to a global one.

Sign up to request clarification or add additional context in comments.

Comments

1

My understanding is that Python is effectively interpreted line by line

That's where you're wrong. The whole file is compiled to bytecode before any interpretation begins.

Also, even if the bytecode compilation pass didn't exist, print(a + 2) wouldn't actually be executed before a = 7 is seen, because it's in a function definition. Python would still know about the a = 7 by the time it actually tries to execute print(a + 2).

2 Comments

Maybe OP meant by the statement that byte codes are executed instruction-by-instruction which is right. Isn't it?
@direprobs: It's not quite as right as you might think, since function calls (explicit, or implicit in something like +) will commonly lead to bytecode instructions executing in the middle of other bytecode instructions, but anyway, I think the questioner really did mean line by line.
0

As per document

A special quirk of Python is that – if no global statement is in effect – assignments to names always go into the innermost scope. Assignments do not copy data — they just bind names to objects.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.