classification
Title: Misleading UnBoundLocalError on assignment to closure variable
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.6
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: eric.smith, josh.r, kolia, steven.daprano
Priority: normal Keywords:

Created on 2019-07-11 19:34 by kolia, last changed 2019-07-12 10:57 by eric.smith. This issue is now closed.

Messages (5)
msg347703 - (view) Author: kolia (kolia) Date: 2019-07-11 19:34
def outer(a):   
    def inner():
      print(a)  
      a = 43    
    return inner
                
t = outer(42)   
                
print(t())      


Outputs:

~/Documents/repro.py in inner()                                   
      1 def outer(a):                                             
      2     def inner():                                          
----> 3       print(a)                                            
      4       a = 43                                              
      5     return inner                                          
                                                                  
UnboundLocalError: local variable 'a' referenced before assignment


This is misleading, since `a` is actually in scope on line 3. What is making it fail is the assignment on line 4, since `a` has not been declared `nonlocal`.

Instead, the error should point to line 4 and report an illegal assignment to a read-only closure variable.
msg347706 - (view) Author: Steven D'Aprano (steven.daprano) * (Python committer) Date: 2019-07-11 20:59
The behaviour and error message is correct, and your interpretation is incorrect. You are not assigning to a closure variable on line 4; you are printing an unbound local variable on line 3, precisely as the error message says. That may not match your *intention*, but Python doesn't do what we want it to do, only what we tell it to do :-)

Like global variables, nonlocals (closures) are NOT defined by "does this name match a name in the surrounding scope?". Rather, *local* variables are defined by assignment: any assignment in the body of the function defines a local, unless otherwise declared as nonlocal or global.


> Instead, the error should point to line 4 and report an illegal assignment to a read-only closure variable.

That can't happen without a huge and backwards-incompatible change to Python semantics. (Possibly in Python 5000?)

The only reason I'm not closing this as "Not a bug" is in case someone would like to suggest an improvement to error message. Perhaps:

UnboundLocalError: local variable 'a' referenced before assignment -- did you forget a nonlocal or global declaration?
msg347709 - (view) Author: Steven D'Aprano (steven.daprano) * (Python committer) Date: 2019-07-11 21:15
To clarify further, unlike (say) Lua, Python doesn't allow variables to change scope part-way through a function. (At least not without hacking the byte-code.) In any function, a name refers to precisely one of (1) a local, (2) a nonlocal, and (3) a global, so you cannot have something like this:

a = 1
def func():
    print(a)  # refers to global a
    a = a + 1  # make a new local 'a'
msg347711 - (view) Author: Josh Rosenberg (josh.r) * (Python triager) Date: 2019-07-11 22:30
I'm inclined to close as Not a Bug as well. I'm worried the expanded error message would confuse people when they simply failed to assign a variable, and make them try bad workarounds like adding global/nonlocal when it's not the problem, e.g.:

def foo(bar):
    if bar > 0:
        baz = 'a'
    elif bar < 0:
        baz = 'b'
    return baz

Even if baz exists in an outer scope, they almost certainly didn't intend to use it, they just didn't properly account for all the paths to ensure baz is initialized. Adding a global or nonlocal declaration would make calling with zero as the arg "work" if the function was called with a non-zero value first, but it wouldn't be correct. When the error message suggests a fix, people tend to do it without thinking critically (or understanding the underlying problem), which is worse than a message without a fix where solving it involves learning what it actually means.
msg347738 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2019-07-12 10:57
Thanks for the great explanation, Steven. And I agree with Josh that changing the exception text would lead to blindly adding nonlocal or global in a superficial attempt to get the code to work. The much more likely problem is already mentioned: reference before assignment.

So, I'm going to close this.
History
Date User Action Args
2019-07-12 10:57:59eric.smithsetstatus: open -> closed

type: compile error -> behavior

nosy: + eric.smith
messages: + msg347738
resolution: not a bug
stage: resolved
2019-07-11 22:30:14josh.rsetnosy: + josh.r
messages: + msg347711
2019-07-11 21:15:04steven.dapranosetmessages: + msg347709
2019-07-11 20:59:27steven.dapranosetnosy: + steven.daprano
messages: + msg347706
2019-07-11 19:34:49koliacreate