Python Variable Scope, Closures, and the LEGB Rule
Imagine a house with several rooms. Each room has its own set of objects — a lamp on the bedroom nightstand, a knife in the kitchen drawer. You can use items in the room you're standing in, and you can walk to the hallway to grab something shared. But you can't reach into someone else's closed room without opening the door first.
Python variables work the same way. Every variable lives in a scope — a specific region of your code where that variable exists and can be used. Understanding scope is the key to writing clean functions that don't accidentally overwrite each other's data.
In this tutorial, you'll learn the four scope levels in Python (the LEGB rule), how to use the global and nonlocal keywords, and how closures let inner functions remember values from their parent. By the end, you'll be able to debug the most confusing "variable not found" errors with confidence.
What Is Local Scope?
When you create a variable inside a function, it only exists inside that function. This is called local scope. Think of it as a private room — nothing outside the function can see or touch it.
The variable message is created inside greet(), so it's local to that function. Once greet() finishes, message is gone. If you try to use it outside, Python raises a NameError.
Each function call creates a fresh set of local variables. They don't carry over between calls:
What Is Global Scope?
A variable created outside any function lives in the global scope. It's like an item in the hallway of your house — every room can see it.
Both functions can read the global variable language. But what happens if you try to change it inside a function?
This crashes with an UnboundLocalError. Python sees counter = ... inside the function and assumes counter is a local variable. But then the right side tries to read counter before it has been assigned locally. Python gets confused.
The LEGB Rule: How Python Finds Variables
When you use a variable name, Python searches four places in a specific order. This is called the LEGB rule:
print, len, range.Python checks these in order: L first, then E, then G, then B. It stops at the first match. If it finds nothing in any of them, you get a NameError.
Each print(x) finds a different x because it searches from its own scope outward. The inner function finds its local x first and stops looking.
The global Keyword
If you need to modify a global variable from inside a function, you must tell Python explicitly with the global keyword. This is like getting a special key to reach into the hallway and move things around.
The global score declaration tells Python: "I'm not creating a new local variable. I want to use the global one." Now the assignment score = score + points works correctly.
total = 0
def add(n):
global total
total = total + n
add(5)
add(3)
print(total) # 8def add(current, n):
return current + n
total = 0
total = add(total, 5)
total = add(total, 3)
print(total) # 8Enclosing Scope and Closures
When you define a function inside another function, the inner function can access variables from the outer function. This outer function's scope is called the enclosing scope — the "E" in LEGB.
Something remarkable happens here. make_greeting("Hello") finishes running and returns the inner greet function. Normally, the local variable greeting would be destroyed. But the inner function still needs it!
Python keeps greeting alive because greet references it. This combination of a function plus the variables it remembers from its enclosing scope is called a closure.
Closures are useful for creating specialized functions. Here's a practical example — a multiplier factory:
The nonlocal Keyword
Just like global lets you modify global variables, nonlocal lets you modify variables in the enclosing scope. Without it, assigning to the variable would create a new local one instead.
Without nonlocal count, the line count = count + 1 would crash with an UnboundLocalError — the same problem we saw with globals. The nonlocal keyword tells Python to look one scope outward for the variable.
This pattern is powerful. The count variable is completely hidden from the outside world. No code outside make_counter can access or tamper with it. This is a form of encapsulation — keeping data private.
Common Scope Mistakes and How to Fix Them
Here are the scope bugs that trip up Python developers most often. Knowing these patterns will save you hours of debugging.
Mistake 1: Shadowing a Global Variable
The items = [] inside the function creates a brand new local variable that shadows (hides) the global one. The global items list is never modified. This is usually a bug.
Mistake 2: UnboundLocalError from Mixing Read and Write
Python scans the entire function body before running it. If it sees any assignment to total, it marks total as local for the whole function. Then the print(total) line tries to read a local variable that hasn't been assigned yet.
Practice Exercises
Time to put scope and closures into practice. These exercises progress from reading scope to building your own closures.
Read this code carefully and predict what it prints. Then write a single print() statement that outputs the exact result.
x = 10
def foo():
x = 20
print(x)
foo()
print(x)Print the two numbers on separate lines, exactly as the code above would output them.
This code tries to increment a global counter, but it crashes with an UnboundLocalError. Fix it so the output is:
1
2
3Write a function called make_greeter that takes a greeting string as an argument and returns a new function. The returned function should take a name argument and print the greeting with the name.
For example:
hello = make_greeter("Hello")
hello("World")Should print: Hello, World!
The test will call:
hi = make_greeter("Hi")
hi("Alice")
hi("Bob")Expected output:
Hi, Alice!
Hi, Bob!Write a function called make_counter that takes a start value and returns a function. Each time the returned function is called, it should print the current count and then increment it by 1.
The test will call:
c = make_counter(1)
c()
c()
c()Expected output:
1
2
3Write a function called make_averager that returns a function. Each time the returned function is called with a number, it should print the running average of all numbers seen so far.
The running average is the sum of all numbers divided by the count.
The test will call:
avg = make_averager()
avg(10)
avg(20)
avg(30)Expected output:
10.0
15.0
20.0Hint: the averager needs to remember both the total sum and the count of numbers.
Summary
| Here's what you've learned about Python scope: | |
|---|---|
| --- | --- |
| Local scope | Variables created inside a function, exist only there |
| Global scope | Variables created at the module level, visible everywhere |
| LEGB rule | Python searches Local, Enclosing, Global, Built-in — in that order |
global keyword | Lets a function modify a module-level variable |
| Enclosing scope | An outer function's variables, accessible by inner functions |
| Closure | A function that remembers variables from its enclosing scope |
nonlocal keyword | Lets an inner function modify an enclosing variable |
The golden rule: prefer passing arguments and returning values over using global or nonlocal. Closures are the one exception where enclosing scope shines — they let you create specialized functions with private state.
What's Next?
Now that you understand scope and closures, you're ready for [Recursion](/python/python-recursion) — where functions call themselves. Understanding scope is essential for recursion because each recursive call gets its own local scope.