Python Scope Explained: LEGB Rule, global and nonlocal
Where Does a Variable Live?
You create a variable inside a function, then try to print it outside, and Python says it is not defined. You try to update a counter from inside a function, and Python raises an UnboundLocalError even though the variable clearly exists. Both puzzles come from scope: the rules that decide where a name can be seen and changed. This tutorial explains Python scope step by step: local and global variables, the LEGB rule, the global and nonlocal keywords, the famous UnboundLocalError, closures, scope in loops and comprehensions, shadowing built-in names, and good habits that avoid scope bugs. It builds on the tutorial on Python functions. Every output shown was produced by running the code on Python 3.13.
Names and Namespaces
In Python, a variable is a name that refers to an object. Names are stored in namespaces, which are like dictionaries mapping names to objects. Different parts of a program have different namespaces, and the scope of a name is the region of code where it can be used without any prefix.
Local and Global Scope
- A name assigned at the top level of a file (outside every function) is global: it can be read anywhere in that file (module).
- A name assigned inside a function is local to that function. It is created when the function runs and disappears when it returns.
school = "StudentTools Academy" # global
def welcome(name):
message = f"Welcome to {school}, {name}!" # local
return message
print(welcome("Ada"))
try:
print(message)
except NameError as e:
print("Error:", e)
Output:
Welcome to StudentTools Academy, Ada! Error: name 'message' is not defined
The function can read the global school, but message exists only inside welcome(). Function parameters, like name, are local too.
Same name, different variables
x = 10
def change():
x = 99 # creates a NEW local x
print("inside:", x)
change()
print("outside:", x)
Output:
inside: 99 outside: 10
Assigning to x inside the function creates a separate local variable. The global x is untouched. This isolation is a good thing: functions cannot accidentally overwrite each other's variables.
The LEGB Rule of Python Scope
When Python meets a name, it searches four scopes in order and uses the first match:
- L — Local: names assigned in the current function.
- E — Enclosing: names in any outer function that contains the current one (for nested functions).
- G — Global: names at the top level of the module.
- B — Built-in: Python's built-in names, such as
print,lenandrange.
If the name is not found in any of them, Python raises a NameError.
level = "global"
def outer():
level = "enclosing"
def inner():
level = "local"
print("inner sees:", level)
inner()
print("outer sees:", level)
outer()
print("module sees:", level)
print("len is built in:", len("LEGB"))
Output:
inner sees: local outer sees: enclosing module sees: global len is built in: 4
If you delete the line level = "local", inner() finds the enclosing value instead, and if you also delete the enclosing one, it finds the global value.
The UnboundLocalError Trap
Here is the bug that confuses almost every Python learner:
count = 0
def add_visitor():
count = count + 1
return count
try:
add_visitor()
except UnboundLocalError as e:
print("Error:", e)
Output:
Error: cannot access local variable 'count' where it is not associated with a value
Why? Python decides each name's scope when it compiles the function, before running it. Because count is assigned somewhere in the function, Python treats it as local throughout the whole function. So on the right-hand side, count + 1 tries to read the local count, which has no value yet.
The rule to remember: if a function assigns to a name anywhere, that name is local everywhere in the function, unless you declare otherwise.
The global Keyword
To rebind a global variable from inside a function, declare it with global:
count = 0
def add_visitor():
global count
count += 1
for _ in range(3):
add_visitor()
print(count)
Output:
3
This works, but use global sparingly. Functions that secretly change global variables are hard to test and debug, because their results depend on hidden state. A cleaner design passes values in and returns results:
def add_visitor(count):
return count + 1
visitors = 0
for _ in range(3):
visitors = add_visitor(visitors)
print(visitors)
Output:
3
Mutating is not rebinding
You do not need global to change the contents of a mutable global object, such as appending to a list. You are not reassigning the name, only modifying the object it refers to.
log = []
def record(event):
log.append(event) # no global needed: the name log is only read
record("login")
record("upload")
print(log)
Output:
['login', 'upload']
The nonlocal Keyword
In nested functions, nonlocal lets the inner function rebind a variable of the enclosing function (not the global one):
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter_a = make_counter()
counter_b = make_counter()
print(counter_a(), counter_a(), counter_a())
print(counter_b())
Output:
1 2 3 1
Without nonlocal, count += 1 would raise the same UnboundLocalError as before. Notice that each call to make_counter() creates a fresh, independent count.
Closures
The counter above is a closure: an inner function that remembers variables from the enclosing scope even after the outer function has finished. Closures are a neat way to create configurable functions:
def make_grader(pass_mark):
def grade(score):
return "Pass" if score >= pass_mark else "Fail"
return grade
university = make_grader(45)
scholarship = make_grader(70)
print(university(50), scholarship(50))
print(university.__closure__[0].cell_contents)
Output:
Pass Fail 45
Each grader carries its own pass_mark, stored in its __closure__. Closures are the foundation of decorators, covered in the tutorial on Python decorators and closures.
The late-binding surprise
Closures look up enclosing variables when they are called, not when they are created:
funcs = [lambda: i for i in range(3)] print([f() for f in funcs]) funcs = [lambda i=i: i for i in range(3)] # capture the current value print([f() for f in funcs])
Output:
[2, 2, 2] [0, 1, 2]
All three lambdas in the first list share the same i, which is 2 by the time they run. A default argument captures the value at creation time.
Scope in Loops, Comprehensions and if Blocks
Unlike C or Java, Python has no block scope. Variables created in a for loop, while loop or if block remain available afterwards, in the same function or module:
for n in range(3):
square = n * n
print(n, square)
if True:
inside_if = "still visible"
print(inside_if)
squares = [k * k for k in range(3)]
print("k" in dir())
Output:
2 4 still visible False
Comprehensions are the exception: their loop variable (k here) has its own scope and does not leak out.
Shadowing Built-in Names
Because built-ins come last in the LEGB search, a global or local name can shadow (hide) a built-in one. This is a common source of confusing errors:
list = [3, 1, 2] # bad idea: hides the built-in list
try:
letters = list("abc")
except TypeError as e:
print("Error:", e)
del list # remove the shadowing name
print(list("abc"))
Output:
Error: 'list' object is not callable ['a', 'b', 'c']
Avoid naming variables list, dict, str, sum, max, input or id. Use descriptive names like scores or total instead.
Good Habits for Python Scope
- Pass data in as arguments and return results. This keeps functions independent and easy to test.
- Use global constants freely, but global variables rarely. Constants written in UPPER_CASE, like
PASS_MARK = 45, are fine to read anywhere. - Prefer a class when several functions need to share and update the same state.
- Keep functions short, so it is always obvious which names are local.
Python Scope: Practice Exercises
- Predict the output:
x = 5, then a function that doesx = x * 2and returnsx, called once. Explain the result. - Fix the function in exercise 1 in two ways: with
global, and by passingxas an argument. - Write
make_multiplier(n)that returns a function multiplying its input byn. - Write a closure-based
make_account(balance)that returnsdepositandwithdrawfunctions sharing the same balance, usingnonlocal.
Answer to 1: it raises UnboundLocalError, because the assignment makes x local throughout the function, so it cannot be read before it has a value.
Conclusion
Python scope follows the LEGB rule: Local, Enclosing, Global, then Built-in. Any name assigned inside a function is local to the whole function, which is the cause of the UnboundLocalError. Use global to rebind a module-level name and nonlocal to rebind a name in an enclosing function, but prefer passing arguments and returning values. Closures remember enclosing variables, loops and if blocks do not create new scopes, comprehensions do, and you should never name a variable after a built-in.