Scope and the LEGB rule

Beginner35 min

Predict which variable a name refers to in Python, use global and nonlocal correctly, and stop shadowing built-ins.

What you will learn
  • Resolve a name using the Local, Enclosing, Global, Built-in order
  • Explain why assigning to a name inside a function makes it local, and fix UnboundLocalError
  • Use global and nonlocal only when rebinding is really needed
  • Avoid shadowing built-in names
On this page

A name in a program is only visible in some places. The scope and lifetime lesson in the fundamentals track explains local and global scope, shadowing and closures in general terms. This lesson is about the specific rules Python uses, because one of them surprises nearly everyone once.

The LEGB rule

When Python meets a name, it looks in up to four places, in order, and uses the first match:

  1. Local: names assigned inside the current function.
  2. Enclosing: names in any function that this function is nested inside.
  3. Global: names assigned at the top level of the module (the file).
  4. Built-in: names Python provides everywhere, such as len, print and sum.
legb_three_x.py
x = "global x"
def outer():
x = "enclosing x"
def inner():
x = "local x"
print("inner sees:", x)
inner()
print("outer sees:", x)
outer()
print("module sees:", x)
Output
inner sees: local x
outer sees: enclosing x
module sees: global x

This shows three different variables all called x. Each x = ... creates a new name in its own function, and a lookup stops at the nearest one. The next example uses one name from each layer:

legb_layers.py
y = "global y"
def outer2():
z = "enclosing z"
def inner2():
w = "local w"
print(w, "|", z, "|", y, "|", len("abc"))
inner2()
outer2()
Output
local w | enclosing z | global y | 3

w is local, z is enclosing, y is global, and len is found last, in the built-ins.

Scope in Python is decided by functions (and modules and classes), not by blocks. A variable first assigned inside an if or for body belongs to the whole function, and a loop variable is still there after the loop ends.

Assignment makes a name local

The rule that causes trouble is this: if a function assigns to a name anywhere in its body, that name is local to the whole function. Python decides this before running a single line of the body.

counter = 0
def broken():
counter = counter + 1
return counter
broken()
Output
UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value

In broken, the assignment counter = ... marks counter as local. On the right-hand side the function then tries to read that local before it has a value, so Python raises UnboundLocalError. The global counter was never consulted. Reading a global without assigning to it works fine; it is the assignment that flips the name.

Two statements let a function rebind a name that lives elsewhere. global counter says “this name is the module-level one”:

global_statement.py
counter = 0
def with_global():
global counter
counter = counter + 1
with_global()
with_global()
print(counter)
Output
2

nonlocal count says “this name is the one in the nearest enclosing function”. It only works for names that already exist there, which is why make_counter defines count first.

nonlocal_statement.py
def make_counter():
count = 0
def step():
nonlocal count
count += 1
return count
return step
first = make_counter()
print(first(), first(), first())
second = make_counter()
print(second())
Output
1 2 3
1

make_counter is a closure: step keeps using count after make_counter has returned, and each call to make_counter creates its own separate count, which is why second() starts again at 1.

Mutating is not rebinding

global is needed only to rebind a name, meaning to make it point at a different object. Changing the contents of an object that the name already refers to needs no declaration.

mutate_vs_rebind.py
items = []
def add(x):
items.append(x)
add(1)
add(2)
print(items)
Output
[1, 2]

add calls a method on the global list, which only reads the name items, so it works. rebind below assigns items = ..., which creates a brand-new local and leaves the global untouched.

rebind_local.py
items = [1, 2]
def rebind():
items = ["new"]
return items
print(rebind())
print(items)
Output
['new']
[1, 2]

If you want the second behaviour to change the global you need global items, though returning the new list is usually better.

Shadowing built-ins

Because built-ins come last in the lookup, any name you define earlier hides them. This is legal but rarely wise.

shadow_builtin.py
numbers = [3, 1, 2]
print(sum(numbers))
sum = 100
print(sum)
del sum
print(sum(numbers))
Output
6
100
6

Assigning sum = 100 at module level put a new sum in the global scope, and the global scope is searched before the built-ins. The built-in function is still there; it is just hidden. In between, calling sum(numbers) would have failed:

numbers = [3, 1, 2]
sum = 100
print(sum(numbers))
Output
TypeError: 'int' object is not callable

del sum removes the global name, and the lookup falls through to the built-in again. The classic version of this bug is a variable called list, max, str or input, which then breaks a later call to the built-in of the same name, sometimes dozens of lines away. Choose names such as numbers, highest or text instead. Linters such as ruff warn about it.

Common pitfalls

  • Assigning to a global without global. The assignment creates a local, and reads of the name earlier in the function fail with UnboundLocalError.
  • A variable assigned on only one branch. if flag: result = "yes" followed by return result fails when flag is false, because result is local but was never given a value.
  • Reading a variable before a later assignment in the same function. print(label) followed by label = "ready" is an UnboundLocalError, even though a global label might exist.
  • nonlocal for a name that does not exist. nonlocal needs the name to be defined in an enclosing function already, and it cannot reach module-level names (use global for those).
  • Shadowing built-ins such as list, sum, max, id and input.
  • A loop that may never run. last assigned inside a for body is unbound if the loop has nothing to visit, and reading it afterwards is an UnboundLocalError.
ExerciseDebug five UnboundLocalError puzzles

Each function below fails with UnboundLocalError when you call it: puzzle1(), puzzle2(False), puzzle3(), puzzle4() and puzzle5([]). Fix every function without changing what it is meant to do.

Input: those five calls. Expected output: after the fixes, 15, no, ready, 1 and an empty line.

total = 10
def puzzle1():
total += 5
return total
def puzzle2(flag):
if flag:
result = "yes"
return result
def puzzle3():
print(label)
label = "ready"
def puzzle4():
count = 0
def bump():
count += 1
bump()
return count
def puzzle5(words):
for word in words:
last = word
return last
Show solution
unbound_fixed.py
total = 10
def puzzle1():
global total
total += 5
return total
def puzzle2(flag):
result = "no"
if flag:
result = "yes"
return result
def puzzle3():
label = "ready"
print(label)
def puzzle4():
count = 0
def bump():
nonlocal count
count += 1
bump()
return count
def puzzle5(words):
last = ""
for word in words:
last = word
return last
print(puzzle1())
print(puzzle2(False), puzzle2(True))
puzzle3()
print(puzzle4())
print(puzzle5([]))
Output
15
no yes
ready
1

Puzzle 1 needs global, because += is an assignment. Puzzle 2 gives result a value on every path. Puzzle 3 assigns before reading. Puzzle 4 needs nonlocal so bump rebinds the outer count instead of creating its own. Puzzle 5 gives last a starting value, so an empty list returns "" instead of failing. In real code, puzzle 1 would be better written as def puzzle1(total): return total + 5.

ExercisePredict the output

Without running it, say what this prints, then check. Input: none. Expected output: two lines.

predict_scope.py
level = "global"
def show():
print(level)
def change():
level = "local"
show()
change()
print(level)
Show solution
global
global

show looks up level in its own scope, then in the global scope, where it finds "global". It does not matter that it was called from inside change. Python resolves names by where the function is written (lexical scope), not by who called it. The level inside change is a separate local that show cannot see.

ExerciseFind the shadowed built-in

This program should print the largest of three scores, but it crashes. Rename the variable that causes the problem.

Input: none. Expected output: 95.

max = 80
scores = [72, 95, 88]
print(max(scores))
Output
TypeError: 'int' object is not callable
Show solution
shadow_max_fixed.py
pass_mark = 80
scores = [72, 95, 88]
print(max(scores))
Output
95

The variable max hid the built-in function. Giving it a name that says what it holds, pass_mark, leaves max alone.

Summary

  • Names are looked up Local, Enclosing, Global, Built-in, and the first match wins.
  • Assigning to a name anywhere in a function makes it local to that function. Use global or nonlocal to rebind an outer name, and avoid them where a parameter and a return value will do.
  • Do not name your variables after built-ins such as list, sum or max.

Next: lambdas and higher-order functions, where functions themselves become values.