The following is a reply to a 2023 LP5E book-errata post, relocated here from the official publisher errata page. The errata page breaks indentation in code, and doesn't support meatier replies like this one. To view the original post, click the button below.
name = 'global' # del name def func(): name = 'enclosing' class Cls: print(name) # 1st print name = name # Cls.name attr print(name) # 2nd print # running func() outputs: # 1st print => global # 2nd print => global # # but uncommenting del name # throws NameError # # Isn't name supposed to reference # 'enclosing' from func's local scope, # according to LEGB scope lookup protocol? # # python --version => 3.10.6 # Operating System => Ubuntu func()
This post refers to the section Nested Classes: The LEGB Scopes Rule Revisited which is at page 904 in recent printings and 875 earlier. First off, it's impossible to tell for certain what you're trying to show, because all your code's indentation was lost in the post (an issue with this errata system, regrettably).
That said, I believe you have stumbled onto an obscure inconsistency in how Python processes the local scopes of functions and classes, and an old bug in the implementation of class scopes. Specifically, this seems to have already been raised on the Python bug tracker by two reports:
Given that this bug hasn't been resolved in Python for some 14 years, it may now be permanent, so I'll provide some context for readers here. To get started, consider the following snippet, where =>
marks output:
x = 'global' def f(): x = x # error: unbound local reference (statically determined) print(x) # never reached (but if x=x removed, => global) f() class c: x = x # okay: finds in enclosing scope, assigned to attribute print(x) # => global i = c() print(i.x) # => global
The f()
function call throws an UnboundLocalError
exception, as it should: per the book (see Chapter 21's Gotchas), assigning x
classifies it as a local variable statically when the def
is processed, and before any calls. Because this classification applies to the entire function, the reference to x
on the right is in error: local x
is unassigned.
By confusing contrast, though, the class
and c()
instance-creation call work without error: the assignment to x
in c
creates attribute c.x
when the class statement runs (not when the instance is later created), and the reference to x
on the right happily looks up x
in the surrounding global scope—unlike the def
!
This partly reflects timing: functions classify locals for later use (calls), but classes use names immediately (as the class
statement itself runs, line by line) to make attributes. The LEGB rule applies to classes for all other purposes (where the class statement is L); but they differ in their recognition of local (i.e., class-scope) variables.
Now, in your case (assuming I'm recreating it accurately), the same thing happens when classes are nested—an assignment of a global to a same-named local works, and effectively hides the same name in the enclosing scope:
name = 'global' #del name # makes the name=name in Nested1 fail def f(): name = 'enclosing' def nested(): #name = name # error: local name referenced before assignment print(name) # => enclosing (but only if comment out name=name) return nested f()() def f(): name = 'enclosing' class Nested1: name = name # okay: assigns local/attr to global's value! print(name) # => global (now looked up in local/class) return Nested1 f()() # deleting this avoids exc if del name: call time def f(): name = 'enclosing' class Nested2: # but removing name=name makes LEGB work as usual! print(name) # => enclosing (unlike Nested1!) return Nested2 f()()
Here, the name=name
in Nested1
is resolved when Nested1
's code runs (during an f()
call). As an artifact of implementation, the reference to name
on the right skips the enclosing scope and falls back on the global scope—contrary to the LEGB rule.
Further, because the name=name
in the Nested1
class statement simply sets local name
to the value of global name
global, deleting name
from the global scope before this will make this statement fail with an exception, as you noticed.
This exception can happen only if a class relies on a global-scope name that's deleted (rare to be sure). More disturbingly, though, this means that such an assignment won't pick up the same name in the enclosing def
per LEGB, as it probably should.
For example, removing name=name
in Nested2
causes the enclosing scope to be correctly used for its name
reference. This seems buggy; the references to name
in Nested1
and Nested2
should work the same, but something about the name=name
assignment in Nested1
throws this off.
Oddest of all, this occurs only when assigning a name to its identical self—using any name that differs from the global suffices to correctly access the enclosing def
's name:
name1 = 'global' def f(): name1 = 'enclosing' class Nested1: name1 = name1 # skips enclosing scope for name1 ref (bug!) print(name1) # => global return Nested1 f()() name1 = 'global' def f(): name1 = 'enclosing' # different names breaks the spell class Nested3: name2 = name1 # uses enclosing scope for name1 ref (LEGB) print(name2) # => enclosing! return Nested3 f()()
Alternatively, adding nonlocal
statements to both function and class skirt the issue too, by ensuring that all name assignments are mapped to the enclosing def
—and in the same way:
name = 'global' # irrelevant here del name # irrelevant here def f(): name = 'enclosing' def nested(): nonlocal name # adding this makes all lines work name = name # mapped to enclosing def print(name) # => enclosing return nested f()() def f(): name = 'enclosing' class Nested: nonlocal name # adding this makes all lines work, per LEGB name = name # mapped to enclosing def print(name) # => enclosing return Nested f()()
Hence, you might avoid this drama by adding nonlocal
to forcibly map to the enclosing def
. Better yet, don't use the same name for variables in multiple scopes; that's confusing even on a good day (this post did so as a demo, but real code should do better).
Thanks for the interesting query. This seems too obscure to cover in the book and may be fixed in Python at any time, but it may warrant a perplexing sidebar in a future edition. For now, feel free to escalate on the Python bugs tracker if you care enough to do so.