LEGB
A name is looked up in that order and the search stops at the first hit. That is also the danger: calling a variable list or id at module level shadows the built-in for the whole file, and the error appears somewhere else entirely.
| Scope | Example | |
|---|---|---|
| L | Local — inside this function | a parameter, or anything assigned in the body |
| E | Enclosing — a function around this one | the variable a closure captured |
| G | Global — this module | a name defined at the top of the file |
| B | Built-in | len, print, list |
Assignment makes a name local — for the whole function
Python decides at compile time: if a function assigns to a name anywhere in its body, that name is local everywhere in the body, including on lines before the assignment. Reading it first is an UnboundLocalError, not a fall back to the global.
Mutating is not assigning — items.append(x) works on a global list without any declaration, because it never rebinds the name. items = items + [x] does rebind, and fails.
count = 0
def bump():
count = count + 1 # UnboundLocalError
# ^ reading a local that has not been assigned yet
def bump():
global count # "this name is the module's one"
count = count + 1
def show():
print(count) # fine — reading a global needs no declaration
Closures, and nonlocal
A closure is a function that remembers the variables of the scope it was defined in — not their values, the variables. global reaches the module; nonlocal reaches the nearest enclosing function. There is no way to reach two levels up by name.
def counter():
n = 0
def tick():
nonlocal n # the enclosing n, not a new local one
n += 1
return n
return tick
t = counter()
t(), t(), t() # 1, 2, 3
# the captured variable lives as long as the inner function does
t.__closure__[0].cell_contents # 3
The late-binding loop
The closure captured the variable i, and by the time anything was called the loop had finished with it at 2. Default arguments are evaluated at definition time, which is exactly the property that fixes it here — the same rule that caused the mutable-default bug in the last lesson.
handlers = []
for i in range(3):
handlers.append(lambda: i)
[h() for h in handlers] # [2, 2, 2] — every lambda sees the same i
# fix 1: bind the value now, as a default argument
handlers = [lambda i=i: i for i in range(3)]
# fix 2: a factory, which makes a genuinely new scope each time
def make(i):
return lambda: i
handlers = [make(i) for i in range(3)]
[h() for h in handlers] # [0, 1, 2]
Two smaller rules
- A comprehension has its own scope.
[x for x in items]leaves noxbehind; theforloop it replaces does. - A class body is not an enclosing scope. A method cannot see a class attribute by bare name — it is
self.thingorClassName.thing. globalandnonlocalare declarations, not assignments. They change where the name lives; they do not give it a value.