Scope & Closures
When you use a variable name, Python has to decide which variable you mean. A name created inside a function is not the same as the same name outside it. The rules that decide where a name can be seen are called scope.
Understanding scope will save you from confusing bugs such as UnboundLocalError. It also unlocks closures, functions that remember values, which you will use again in Decorators.
Local and global variables
A variable created inside a function is local. It exists only while the function runs and cannot be seen from outside. A variable created at the top level of a file is global: every function in that file can read it.
shop_name = "Meera's Bakery" # global
def make_bill(amount):
tax = amount * 0.05 # local
print(f"{shop_name}: ₹{amount + tax:.2f}")
make_bill(400)
try:
print(tax)
except NameError as error:
print("Error:", error)Meera's Bakery: ₹420.00 Error: name 'tax' is not defined
The function can read shop_name because it is global. But tax disappeared as soon as make_bill() finished, so the code outside cannot see it. This is a good thing: functions can use simple names like total or i without clashing with the rest of your program.
The LEGB rule
When Python meets a name, it searches four places in this order and uses the first match:
| Letter | Scope | Where it lives |
|---|---|---|
| L | Local | Inside the current function |
| E | Enclosing | Inside any function that contains the current function |
| G | Global | At the top level of the current file (module) |
| B | Built-in | Names Python always provides, such as print, len, list |
If the name is not found in any of them, you get a NameError. The built-in level explains a classic mistake: if you create a variable called list or sum, it hides the built-in function, and later calls like list("abc") fail with "'list' object is not callable".
Changing a global variable
Reading a global variable inside a function is fine. Assigning to it is different. Any assignment inside a function makes that name local for the whole function, so this code fails:
visitors = 0
def add_visitor():
visitors = visitors + 1
try:
add_visitor()
except UnboundLocalError as error:
print("Error:", error)Error: cannot access local variable 'visitors' where it is not associated with a value
Because of visitors = ..., Python treats visitors as local inside the function. The right side then tries to read that local variable before it has a value. You can fix it with the global keyword, which tells Python that the name refers to the global variable:
def add_visitor():
global visitors
visitors = visitors + 1This works, but use global rarely. Functions that quietly change global variables are hard to test and hard to reason about. A cleaner design passes the value in and returns the new value: visitors = add_visitor(visitors).
Nested functions and nonlocal
You can define a function inside another function. The inner function can read the variables of the outer one: this is the enclosing scope, the E in LEGB.
To change an enclosing variable from the inner function, declare it nonlocal. It works like global, but for the nearest enclosing function instead of the module.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
token = make_counter()
print(token(), token(), token())
other_queue = make_counter()
print(other_queue())1 2 3 1
Notice something surprising: make_counter() has already finished when we call token(), yet count is still alive and keeps growing. Each call to make_counter() also creates a separate count, so other_queue starts again from 1.
Closures
A closure is an inner function that remembers variables from the enclosing scope, even after the outer function has returned. increment above is a closure. Closures are a neat way to build customised functions from a template:
def make_discount(percent):
def apply(price):
return round(price * (100 - percent) / 100)
return apply
festival_sale = make_discount(20)
student_offer = make_discount(10)
print(festival_sale(1299))
print(student_offer(1299))
print(festival_sale(450))1039 1169 360
festival_sale and student_offer are two different functions. Each one carries its own value of percent. You did not need a class or a global variable to store the discount.
The late binding trap
A closure remembers the variable, not the value it had when the function was created. The value is looked up when the inner function runs. This matters when you create functions in a loop:
multipliers = []
for n in [1, 2, 3]:
multipliers.append(lambda x: x * n)
print([f(10) for f in multipliers])
fixed = []
for n in [1, 2, 3]:
fixed.append(lambda x, n=n: x * n)
print([f(10) for f in fixed])[30, 30, 30] [10, 20, 30]
All three lambdas in the first loop share the same n, and by the time they run, the loop has finished with n equal to 3. The fix uses a default argument, n=n, which copies the current value when each lambda is created (defaults are evaluated once, as you saw in Function Arguments).
Common mistakes
- Assigning to a global inside a function without
global. You getUnboundLocalError. Better still, return the new value instead of changing a global. - Naming variables after built-ins.
list,dict,sum,inputandidare all built-in names. Useitems,totalsoruser_idinstead. - Expecting a closure to freeze a loop variable. Use a default argument such as
n=nto capture the value. - Using
nonlocalfor a global variable.nonlocalonly works inside nested functions and refers to an enclosing function's variable.
Key points
- Variables created inside a function are local; variables at the top of the file are global.
- Python finds names using the LEGB order: Local, Enclosing, Global, Built-in.
- Assigning to a name inside a function makes it local, unless you declare it
globalornonlocal. - A closure is an inner function that remembers variables from its enclosing function.
- Closures look up variables when they run, so capture loop values with a default argument.
- Use
globalrarely; passing values in and returning results keeps code clear.
Exercise
Write a function make_wallet(balance) that returns a function spend(amount). Each call to spend() should subtract the amount from the remembered balance and return the new balance, but if there is not enough money it should return the message "Not enough balance" and leave the balance unchanged. Create a wallet for Kabir with ₹500, spend ₹120, then ₹300, then ₹200, and print each result.
Show solution
def make_wallet(balance):
def spend(amount):
nonlocal balance
if amount > balance:
return "Not enough balance"
balance -= amount
return balance
return spend
kabir = make_wallet(500)
print(kabir(120))
print(kabir(300))
print(kabir(200))380 80 Not enough balance
spend is a closure over balance. The nonlocal line is needed because spend assigns to balance; without it Python would treat balance as a new local variable and raise UnboundLocalError.
Quick Quiz
1. In which order does Python search for a name?
This is the LEGB rule: Local first, then Enclosing, Global and finally Built-in.
2. A function assigns total = total + 1, and total is a global variable. What happens?
Any assignment makes the name local for the whole function, so it is read before it has a value.
3. Which keyword lets an inner function change a variable of its enclosing function?
nonlocal refers to the nearest enclosing function's variable.
4. What is a closure?
Closures carry variables from their enclosing scope, even after the outer function has returned.
Learn Python with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
