PYTHON / FUNCTIONS
Scope, the LEGB rule, global and nonlocal
Predict which binding a name resolves to in nested Python functions, and rebind names deliberately with global and nonlocal.
What you will learn
- Resolve a name by walking local, enclosing, global, then builtin scopes
- Recognise that any assignment in a function body makes that name local everywhere in it
- Use nonlocal to rebind a name owned by an enclosing function, not a module global
- Use global to rebind a module-level name from inside any function, however deeply nested
Understanding Scope, the LEGB rule, global and nonlocal
When Python evaluates a bare name inside a function it searches four scopes in a fixed order: Local (this function call), Enclosing (any function that lexically wraps it), Global (the module namespace), Builtin (the names in the builtins module such as len and print). The first scope that has the name wins, and the search stops there. Two functions defined side by side are not in each other's scope; only lexical nesting creates an enclosing scope, which is why the E step depends on where the def is written, not on who calls it.
The critical detail is that the L in LEGB is decided at compile time, not while the function runs. When Python compiles a function body it scans for binding operations, an assignment, an augmented assignment, a for target, with ... as, import, a nested def or class, except ... as, or a walrus, and every name bound anywhere in that body becomes local for the whole body. That is why reading a name before its assignment in the same function raises UnboundLocalError instead of falling back to the global value: the compiler already classified the name as local, so the lookup never consults the outer scopes.
global and nonlocal exist purely to override that compile-time classification. global name tells the compiler that assignments to name should write to the module namespace, and it skips straight past any enclosing function, so global inside a nested function never touches the outer function's variable. nonlocal name binds to the nearest enclosing function scope that already contains that name; if no enclosing function binds it, the code is rejected at compile time with a SyntaxError, because there is no cell for the closure to point at. Neither statement creates a value; they only redirect where the assignment lands.
x = "global"
def outer():
x = "enclosing"
def inner():
print("inner sees:", x)
def rebind():
nonlocal x
x = "rebound by nonlocal"
inner()
rebind()
print("outer sees:", x)
def set_global():
global x
x = "set by global"
outer()
print("module sees:", x)
set_global()
print("module now sees:", x)
print(len("abc"), "came from the builtin scope")Python classifies each name in a function as local or non-local when it compiles the function, and global and nonlocal are the only ways to change that classification.
Worked examples
Assignment turns a name local before the first line runs
Reading a module-level name in a function that also assigns to it fails, and global fixes it.
count = 0
def broken():
print("about to read count:", count)
count = count + 1
try:
broken()
except UnboundLocalError:
print("UnboundLocalError raised, nothing was printed")
def fixed():
global count
print("about to read count:", count)
count = count + 1
fixed()
print("module count:", count)Example explained
Line 1count = count + 1 on the last line of broken makes count local for the entire function.
Line 2So the print on the first line of broken already fails; no output appears from it.
Line 3global count in fixed redirects both the read and the write to the module namespace.
Line 4After fixed() returns, the module-level count really changed to 1.
nonlocal gives each closure its own private state
Two counters built from the same factory keep independent enclosing variables.
def make_counter():
n = 0
def tick():
nonlocal n
n += 1
return n
return tick
a = make_counter()
b = make_counter()
print(a(), a(), a())
print(b())
print(a.__closure__[0].cell_contents)Example explained
Line 1Each make_counter() call creates a fresh n, so a and b never share state.
Line 2nonlocal n makes n += 1 rebind the enclosing n instead of creating a local one.
Line 3Arguments are evaluated left to right, so a(), a(), a() prints 1 2 3.
Line 4a.__closure__[0].cell_contents shows the cell object that actually holds the enclosing n.
Local and global names shadow builtins
The B in LEGB is searched last, so your own names silently hide builtin functions.
def total(values):
sum = 0
for v in values:
sum += v
return sum
print(total([1, 2, 3]))
print(sum([1, 2, 3]))
list = [1, 2]
try:
print(list(range(3)))
except TypeError as e:
print("TypeError:", e)Example explained
Line 1Inside total, sum is a local integer, so the builtin sum is unreachable there.
Line 2Outside total the local is gone, so sum([1, 2, 3]) finds the builtin again and prints 6.
Line 3list = [1, 2] binds a global name that shadows the builtin list for the rest of the module.
Line 4Calling it then fails, because the name now resolves to a list object rather than the type.
Important notes
Mutating an object, such as items.append(1) or d["k"] = 1, is not a binding, so it needs no global or nonlocal; only rebinding the name itself does.
A comprehension or generator expression has its own function scope, so assignments to its loop variable never leak out, and a class body is not an enclosing scope for methods defined inside it.
Common mistakes
Using global inside a nested function while expecting it to change the outer function's variable; the outer variable stays untouched and a module-level name is created or overwritten instead.
Assuming a read of a global works as long as the assignment comes later in the function; the read raises UnboundLocalError because the compiler already marked the name local.
Writing nonlocal for a name that only exists at module level; the file fails to compile with SyntaxError: no binding for nonlocal 'name' found, so nothing runs at all.
Try it yourself
Change, predict, then run
Write make_account(balance) that returns a deposit(amount) function using nonlocal to update balance, and have deposit also increment a module-level DEPOSIT_COUNT declared with global. Create two accounts, make three deposits across them, and print both balances and DEPOSIT_COUNT.
Open the Python workspaceCheck your understanding
Module level has msg = "module". Inside outer, msg = "outer"; a nested inner declares global msg and assigns "inner"; outer calls inner() and then returns msg. What happens?
- outer() returns "outer" and the module-level msg becomes "inner"
- outer() returns "inner" and the module-level msg stays "module"
- outer() returns "outer" and the module-level msg stays "module"
- It fails to compile, because msg is already bound in the enclosing scope
Show answer
global skips every enclosing function scope and points the assignment at the module namespace, so inner overwrites the module-level msg while outer's own local msg is untouched, giving "outer" as the return value. Option 2 is tempting if you read global as "the nearest outer scope", but that is what nonlocal does; option 3 would only be true if global had no effect at all.