Variable Scope and the global Keyword in Python
A clear guide to Python variable scope — local, enclosing, global, and built-in names, the LEGB rule, and when to reach for the global and nonlocal keywords.
What you'll learn
- ✓What scope means and how Python finds a name
- ✓The LEGB rule: Local, Enclosing, Global, Built-in
- ✓When and why to use the global keyword
- ✓How nonlocal lets nested functions write to enclosing scope
- ✓Why most "scope problems" are really mutation vs reassignment
Prerequisites
- •Comfortable with functions — see Functions
Every name in Python — variable, function, class — lives in some scope. Most of the time the rules are invisible: you write a function, it uses its parameters and locals, and everything just works. But when a function needs to read or modify something defined outside it, the scoping rules matter. This post covers Python’s four scopes, the LEGB lookup rule, and the keywords that let you opt out of the defaults.
What scope is
A scope is a region of code where a particular set of names is visible. When you write x in your code, Python decides which x you mean based on the scope where that line lives.
x = 10
def f():
print(x) # reads the module-level x
f() # 10
The function f does not define its own x, so Python looks outward and finds the module-level one. So far, so reasonable.
The four scopes
Python recognises four scopes, traditionally remembered with the acronym LEGB:
- L — Local: names defined inside the current function
- E — Enclosing: names in any enclosing function (for nested functions)
- G — Global: names at the top level of the current module
- B — Built-in: names that come from Python itself (
print,len,range, …)
When Python evaluates a name, it searches L, then E, then G, then B, and uses the first match. If none of them has the name, you get NameError.
n = 100 # Global
def outer():
m = 50 # Enclosing for inner
def inner():
k = 10 # Local
print(k, m, n, len("hi")) # Local, Enclosing, Global, Built-in
inner()
outer() # 10 50 100 2
Every name in that print came from a different scope.
Local by default on assignment
Here is the rule that surprises people: assigning to a name inside a function makes it local, even if there is a same-named variable in an outer scope.
count = 0
def increment():
count = count + 1 # UnboundLocalError
increment()
Python sees the assignment to count and decides it is a local. Then count + 1 tries to read a local that has not been assigned yet — hence UnboundLocalError.
This is not a bug; it is what keeps functions self-contained. The fix depends on what you really want:
- If you want to modify the global, use
global. - If you want a local independent of the global, give it a different name.
count = 0
def increment():
global count
count = count + 1
increment()
print(count) # 1
When to use global
In short: rarely. Global state makes functions harder to test and reason about, because their output depends on more than their inputs.
The reasonable uses are:
- A module-level cache or registry that genuinely belongs to the module
- A configuration value set during startup
- A small script where the alternative is over-engineered
For anything else, prefer passing values in and returning new values out:
# Discouraged
total = 0
def add_to_total(n):
global total
total += n
# Preferred
def add(running_total, n):
return running_total + n
The second form has no hidden state. You can call it from any test, in any order, and the result is determined entirely by its arguments.
Reading vs writing — the asymmetry
A function can read any outer name without global. It only needs global to rebind one. This catches people because mutating a mutable object is reading-the-name, not rebinding it:
config = {"theme": "dark"}
def update_theme(value):
config["theme"] = value # mutates the dict — no global needed
update_theme("light")
print(config) # {'theme': 'light'}
Compare this to:
config = {"theme": "dark"}
def replace_config(new_config):
config = new_config # rebinds local "config" only
replace_config({"theme": "light"})
print(config) # {'theme': 'dark'} — unchanged
The first function mutates the existing dictionary. The second rebinds a local name and the caller never sees it. The distinction is the same one we saw with arguments in Python Functions.
Try it yourself. Predict the output of each version:
# Version A
items = [1, 2, 3]
def add_item(x):
items.append(x)
add_item(4)
print(items)
# Version B
items = [1, 2, 3]
def replace_items(new):
items = new
replace_items([10, 20])
print(items)
# Version C
items = [1, 2, 3]
def replace_items(new):
global items
items = new
replace_items([10, 20])
print(items)Run them and explain to yourself why each one behaves the way it does.
nonlocal for nested functions
Inside a nested function, global skips over the enclosing function. To rebind a name in the enclosing function’s scope, use nonlocal:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
Without nonlocal, the inner count += 1 would treat count as local and raise UnboundLocalError. nonlocal tells Python: “the count I am assigning to is the one defined in an enclosing function, not a new local.”
nonlocal is most common in closures, decorators, and callback patterns. Like global, prefer not to need it — but it has a clear and limited purpose when you do.
Built-in scope and shadowing
The built-in scope holds names like print, len, range, list, dict, min, max, and so on. You can shadow any of them by assignment:
list = [1, 2, 3]
print(list) # [1, 2, 3]
# Now list is no longer the constructor
# list((1, 2, 3)) # TypeError
Shadowing builtins is a common cause of confusing bugs. Avoid using these names: list, dict, set, tuple, str, int, float, id, type, input, min, max, sum, len, range, iter, map, filter, zip, open, format, dir, vars. When you need a similar-looking name, suffix it: items, result, values, text.
Modules have one global scope each
The “global” in global is module-global, not program-global. Each .py file has its own top-level namespace. Importing a module gives you access to its names, but does not merge them into yours:
# In one file:
counter = 0
# In another:
from other_module import counter
counter = 5 # rebinds *this* module's name, not other_module.counter
This is healthy isolation. The Modules and Imports post later in the series covers the import system in detail.
A note on class and comprehension scope
Two small extras you will eventually meet:
- A
classbody is a scope, but its names are not visible to methods unless accessed viaselfor the class name. We will cover this in the classes posts. - Comprehensions (
[x for x in ...]) have their own scope, so the loop variable does not leak. In Python 3, this:
x = "original"
squares = [x * x for x in range(5)]
print(x) # "original" — comprehension didn't leak
This was not always the case in Python 2 — another reason to write Python 3 code only.
Try it yourself. Write make_adder(n) that returns a function which adds n to its argument. Use the enclosing scope; you should not need nonlocal because you are only reading n, not rebinding it. Confirm make_adder(10)(5) returns 15.
A worked example: a small accumulator
A self-contained example using LEGB, closures, and nonlocal:
def make_stats():
"""Return a pair (add, summary) for accumulating numbers."""
values = []
total = 0
def add(n):
nonlocal total
values.append(n)
total += n
def summary():
if not values:
return {"count": 0, "total": 0, "mean": None}
return {
"count": len(values),
"total": total,
"mean": total / len(values),
}
return add, summary
add, summary = make_stats()
for n in [3, 7, 5, 9, 4]:
add(n)
print(summary())
# {'count': 5, 'total': 28, 'mean': 5.6}
values.append(n) is a mutation — no nonlocal needed. total += n is a rebinding — nonlocal is required. This is the asymmetry from earlier in the post in one example.
Recap
You now know:
- A scope is a region where particular names are visible
- Python searches scopes in LEGB order: Local, Enclosing, Global, Built-in
- Assignment inside a function makes the name local by default
- Use
globalto rebind a module-level name from inside a function — sparingly - Use
nonlocalto rebind an enclosing function’s name from a nested one - Reading a name is unrestricted; only rebinding requires
globalornonlocal - Avoid shadowing builtins like
list,dict,sum,len
Next steps
With scope under your belt, the next big topic is robustness: what to do when something goes wrong. The next post covers Python’s exception system — try, except, else, and finally — and the habits that make error handling work for you rather than against you.
→ Next: Error Handling in Python — try, except, else, finally
Questions or feedback? Email codeloomdevv@gmail.com.