Variable Scope (LEGB)
Scope determines where in your code a variable name is visible and usable. When Python encounters a name, it doesn't search randomly — it follows a strict, predictable order known as the LEGB rule: Local, Enclosing, Global, Built-in.
The LEGB Rule
Local— names assigned inside the current function.Enclosing— names in any enclosing (outer) function, for nested functions.Global— names assigned at the top level of the module (the file).Built-in— names Python provides automatically, likelen,print,str.
When you reference a name, Python looks in the Local scope first. If it's not found there, it checks Enclosing, then Global, then Built-in — stopping at the first match. If none of the four levels has the name, Python raises a NameError.
Worked Example: Resolving a Name Through Each Level
x = "global x" # Global scope
def outer():
x = "enclosing x" # Enclosing scope (relative to inner)
def inner():
x = "local x" # Local scope
print(x) # Local found first -> "local x"
inner()
outer()
print(x) # refers to the Global x, unaffected by the functions abovelocal x global x
Now remove the local x from inner() and watch Python fall back to Enclosing:
x = "global x"
def outer():
x = "enclosing x"
def inner():
print(x) # no local x -> checks Enclosing -> found "enclosing x"
inner()
outer()enclosing x
And if neither inner nor outer defines x at all, Python falls all the way back to Global:
x = "global x"
def outer():
def inner():
print(x) # no local, no enclosing -> Global -> "global x"
inner()
outer()global x
Finally, names like print or len are never assigned anywhere in your code, so Python resolves them at the last level: Built-in.
print(len("hello")) # both 'print' and 'len' resolve via Built-in scope5
Modifying Globals with `global`
By default, assigning to a name inside a function creates a new local variable — it does not modify a global of the same name. To explicitly modify a global variable from inside a function, declare it with the global keyword first.
counter = 0
def increment_broken():
counter = counter + 1 # UnboundLocalError: counter is treated as local
def increment_fixed():
global counter
counter = counter + 1
increment_fixed()
increment_fixed()
print(counter) # 22
Without global counter, Python decides at compile time that counter inside increment_broken is local (because it's assigned to), which conflicts with reading it on the right-hand side before it has a local value — raising UnboundLocalError.
Modifying Enclosing Scope with `nonlocal`
global only reaches the module-level scope. To modify a variable in an enclosing function (not global, just one level up), use nonlocal instead.
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()) # 31 2 3
Shadowing Built-in Names
list = [1, 2, 3] # shadows the built-in 'list' type print(list) # [1, 2, 3] — looks fine so far... numbers = list(range(5)) # TypeError: 'list' object is not callable
[1, 2, 3] TypeError: 'list' object is not callable
The fix is simply to pick a different variable name — items, values, my_list — that doesn't collide with a built-in.
Quick Reference
Scope | Where defined | Keyword to modify from inner function |
|---|---|---|
Local | Inside the current function | n/a (default for assignment) |
Enclosing | In an outer function (nested functions) |
|
Global | Top level of the module |
|
Built-in | Provided by Python itself | n/a (avoid shadowing instead) |
total = 100 # Global
def process():
total = 50 # Enclosing (relative to adjust)
def adjust():
total = 10 # Local
print("Local total:", total)
print("Built-in len:", len([1, 2, 3])) # Built-in scope
adjust()
print("Enclosing total:", total)
process()
print("Global total:", total)Local total: 10 Built-in len: 3 Enclosing total: 50 Global total: 100