Scope confused me for a surprisingly long time when I was learning Python. I’d write a function, try to modify a variable defined outside it, and get an error that made no sense to me at the time. It wasn’t until I understood Python’s scoping rules — and the famous LEGB rule — that things finally fell into place. This guide is my attempt to explain scope and binding the way I wish it had been explained to me.
What Is Variable Scope?
Scope refers to the region of a program where a particular variable name is recognized and accessible. Python determines a variable’s scope based on where it’s assigned in the code, not where it’s used — this is a static, compile-time determination, even though Python is an interpreted language.
The LEGB Rule
Python resolves variable names using a lookup order commonly remembered as LEGB:
- Local — names assigned within the current function.
- Enclosing — names in any enclosing function (relevant for nested functions/closures).
- Global — names assigned at the top level of the module.
- Built-in — names provided by Python itself, like
len,print,range.
Python searches these scopes in that exact order, using the first match it finds.
x = "global x"
def outer():
x = "enclosing x"
def inner():
x = "local x"
print(x)
inner()
outer()
Output:
local x
If I remove the local assignment inside inner(), Python looks one level up:
x = "global x"
def outer():
x = "enclosing x"
def inner():
print(x)
inner()
outer()
Output:
enclosing x
And if I remove that too, it falls back to global:
x = "global x"
def outer():
def inner():
print(x)
inner()
outer()
Output:
global x
Local Scope
Variables assigned inside a function are local to that function by default, and they don’t exist outside it.
def greet():
message = "Hello!"
print(message)
greet()
print(message)
Output:
Hello!
Traceback (most recent call last):
...
NameError: name 'message' is not defined
This is one of the first scope-related errors most people hit — message only exists within greet()‘s local scope.
Global Scope
Variables assigned at the top level of a script or module belong to the global scope and are accessible from anywhere within that module, including inside functions — for reading.
count = 10
def show_count():
print(count)
show_count()
Output:
10
Reading a global variable inside a function works without any special keyword. Things change, however, when I try to reassign a global variable from inside a function.
Why You Can’t Modify Globals Without the global Keyword
count = 10
def increment():
count += 1
print(count)
increment()
Output:
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
This error confused me badly the first time I saw it. The reason is that Python decides, at compile time, that count is a local variable anywhere it’s assigned within the function body — even before that assignment line executes. Since count += 1 includes an assignment, Python treats count as local for the entire function, and reading it before that local assignment happens raises the error.
Using the global Keyword
To explicitly tell Python that a name inside a function refers to the global variable rather than a new local one, I use the global keyword:
count = 10
def increment():
global count
count += 1
print(count)
increment()
print(count)
Output:
11
11
Now the function modifies the actual global variable, and the change persists after the function returns.
The nonlocal Keyword
nonlocal solves a similar problem, but for enclosing (not global) scope — specifically in nested functions, which is common in closures.
def outer():
count = 0
def increment():
nonlocal count
count += 1
print(count)
increment()
increment()
print("Final:", count)
outer()
Output:
1
2
Final: 2
Without nonlocal, the inner function would raise an UnboundLocalError for the same reason described above — Python would treat count as local to increment() because of the assignment.
def outer():
count = 0
def increment():
count += 1 # no nonlocal
print(count)
increment()
outer()
Output:
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
Closures: Where nonlocal Really Shines
Closures are functions that “remember” variables from their enclosing scope even after that scope has technically finished executing. This is a foundational concept for things like decorators and factory functions.
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
my_counter = make_counter()
print(my_counter())
print(my_counter())
print(my_counter())
Output:
1
2
3
Each call to make_counter() creates a completely independent enclosing scope, so multiple counters don’t interfere with each other:
counter_a = make_counter()
counter_b = make_counter()
print(counter_a())
print(counter_a())
print(counter_b())
Output:
1
2
1
Internal Working: How Python Implements Scope
Under the hood, CPython determines variable scope at compile time by analyzing the function’s bytecode. Local variables are stored in a fast array-like structure associated with the function’s stack frame (accessed via LOAD_FAST/STORE_FAST bytecode instructions), which is significantly faster than looking up names in a dictionary. Global and built-in lookups, by contrast, use LOAD_GLOBAL, which involves dictionary lookups in the module’s namespace and then the built-ins namespace if needed.
This is actually one reason local variable access is faster than global variable access in Python — local variables are indexed directly, while globals require a dictionary-based name lookup.
import time
x = 0
def use_global():
global x
for _ in range(1_000_000):
x += 1
def use_local():
local_x = 0
for _ in range(1_000_000):
local_x += 1
return local_x
start = time.time()
use_global()
print("Global:", time.time() - start)
start = time.time()
use_local()
print("Local:", time.time() - start)
In my own tests, the local variable version consistently runs faster, which lines up with how CPython implements variable access internally.
Common Mistakes
- Forgetting the global keyword when reassigning a global variable — Leads to the classic
UnboundLocalError. - Overusing global variables — Functions that rely heavily on global state become harder to test and reason about; I try to pass values as arguments and return results instead wherever practical.
- Confusing nonlocal with global —
nonlocalrefers to the nearest enclosing function scope, not the module-level global scope; using the wrong one raises aSyntaxErrorif there’s no matching enclosing scope. - Shadowing built-in names — Naming a variable
list,str, oridshadows the built-in in that scope, which can cause confusing bugs later in the same scope. - Assuming loop variables have their own scope — Unlike some languages, Python’s
forloop variable persists in the enclosing scope after the loop ends, which occasionally leads to surprises.
Real-World Applications
- Decorators: Rely heavily on closures and
nonlocal(or mutable containers) to maintain state across function calls. - Configuration management: Careful use of module-level globals for application-wide settings, often paired with functions that explicitly update them.
- Caching and memoization: Closures that maintain a cache dictionary across calls without exposing it globally.
- Event-driven programming: Callback functions that capture and modify enclosing state through closures.
Debugging Tips
When I hit an UnboundLocalError, my first move is to check whether the variable is assigned anywhere in the function body, since that’s almost always the cause — Python doesn’t need to see the assignment executed, just present in the code, to treat the name as local.
I also use locals() and globals() for a quick reality check on what’s actually in scope at a given point:
def debug_scope():
x = 1
print(locals())
debug_scope()
print(list(globals().keys())[:5])
FAQs
What’s the difference between global and nonlocal? global refers to the module-level scope; nonlocal refers to the nearest enclosing function scope (not the module level), and is only usable inside nested functions.
Can I read a global variable inside a function without the global keyword? Yes — reading works fine without global. The keyword is only required when you want to reassign the variable inside the function.
Do list comprehensions have their own scope? Yes, since Python 3, comprehensions have their own local scope, separate from the enclosing function, which prevents them from leaking loop variables into the surrounding scope.
Is it bad practice to use global variables? Not inherently, but overusing them makes code harder to test and reason about. I use them sparingly, usually for genuine application-wide configuration or constants.
Summary
Understanding variable scope and binding — through the LEGB rule, the distinction between global and nonlocal, and how closures capture enclosing state — has been one of the most valuable things I’ve learned for writing correct, predictable Python code. It also explains performance characteristics, like why local variable access tends to be faster than global access, and lays the groundwork for understanding more advanced patterns like decorators and closures.
