Scope and the LEGB rule
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:
- Local: names assigned inside the current function.
- Enclosing: names in any function that this function is nested inside.
- Global: names assigned at the top level of the module (the file).
- Built-in: names Python provides everywhere, such as
len,printandsum.
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)inner sees: local xouter sees: enclosing xmodule sees: global xThis 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:
y = "global y"
def outer2(): z = "enclosing z"
def inner2(): w = "local w" print(w, "|", z, "|", y, "|", len("abc"))
inner2()
outer2()local w | enclosing z | global y | 3w 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()UnboundLocalError: cannot access local variable 'counter' where it is not associated with a valueIn 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”:
counter = 0
def with_global(): global counter counter = counter + 1
with_global()with_global()print(counter)2nonlocal 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.
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())1 2 31make_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.
items = []
def add(x): items.append(x)
add(1)add(2)print(items)[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.
items = [1, 2]
def rebind(): items = ["new"] return items
print(rebind())print(items)['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.
numbers = [3, 1, 2]print(sum(numbers))
sum = 100print(sum)
del sumprint(sum(numbers))61006Assigning 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 = 100print(sum(numbers))TypeError: 'int' object is not callabledel 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 withUnboundLocalError. - A variable assigned on only one branch.
if flag: result = "yes"followed byreturn resultfails whenflagis false, becauseresultis local but was never given a value. - Reading a variable before a later assignment in the same function.
print(label)followed bylabel = "ready"is anUnboundLocalError, even though a globallabelmight exist. nonlocalfor a name that does not exist.nonlocalneeds the name to be defined in an enclosing function already, and it cannot reach module-level names (useglobalfor those).- Shadowing built-ins such as
list,sum,max,idandinput. - A loop that may never run.
lastassigned inside aforbody is unbound if the loop has nothing to visit, and reading it afterwards is anUnboundLocalError.
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 lastShow solution
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([]))15no yesready1Puzzle 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.
Without running it, say what this prints, then check. Input: none. Expected output: two lines.
level = "global"
def show(): print(level)
def change(): level = "local" show()
change()print(level)Show solution
globalglobalshow 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.
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 = 80scores = [72, 95, 88]print(max(scores))TypeError: 'int' object is not callableShow solution
pass_mark = 80scores = [72, 95, 88]print(max(scores))95The 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
globalornonlocalto 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,sumormax.
Next: lambdas and higher-order functions, where functions themselves become values.