Python Variable Scope: Local, Global, Enclosing, and Built-in Names
Learn Python variable scope, LEGB name lookup, local and global assignments, enclosing scopes, nonlocal, mutation, shadowing, and common scope errors.
Python scope is the part of a program where a name is available for lookup. Understanding scope explains why a variable can be used in one function but not another, why an assignment can cause UnboundLocalError, and how nested functions retain state.
A variable name and the object it refers to are different things. In count = 3, count is a name, and the integer object 3 is the value currently bound to that name. Name binding is the association between an identifier and an object. Scope describes where Python can resolve that identifier.
How Python Scope Works
A name becomes available through an operation such as assignment, a function parameter, an import, a function definition, or an explicit global or nonlocal declaration. Python determines the relevant scope from the location and context where the name is created, assigned, or declared.
Scope applies to names, not permanently to objects. Several names in different scopes can refer to the same object, and one name can later be rebound to a different object.
message = "first object"
message = "second object"
The second line rebinds message; it does not change the first string object.
Module-Level (Global) Scope
A Python file is a module. A name assigned at the top level of that file normally belongs to that module's global scope. Functions defined in the same module can read that name when no nearer local or enclosing name shadows it.
app_name = "Inventory"
def show_app_name():
print(app_name)
def show_again():
print(app_name)
show_app_name()
show_again()
Both functions read the module-level name. “Global” means global to this module, not automatically universal across every imported module. Another module must access it through an import and the exporting module's namespace, and its own local name with the same spelling can still be independent.
Function-Local Scope
Names assigned inside a function belong to that function's local scope by default. Parameters are local names too. Local names normally exist for the duration of a particular function call.
def make_message():
message = "created here"
print(message)
def use_message():
print(message)
make_message()
use_message() # NameError: name 'message' is not defined
message is local to make_message. A sibling function cannot directly access it. When Python cannot find a requested name in any accessible scope, it raises NameError.
Return a value or pass it as an argument when another function needs it:
def make_message():
return "created here"
def use_message(message):
print(message)
text = make_message()
use_message(text)
Local Shadowing of a Global Name
Shadowing occurs when a nearer scope uses the same name as an outer scope. An assignment inside a function creates a separate local binding unless a scope declaration says otherwise.
status = "global"
def show_status():
status = "local"
print(status)
show_status() # local
print(status) # global
The local name takes precedence inside show_status. Changing that local binding does not replace the module-level binding.
The LEGB Name-Resolution Rule
When Python evaluates a name, it normally searches in this order: Local, Enclosing, Global, Built-in. Python stops at the first matching binding.
| Lookup level | Where Python looks | Typical example | When it applies |
|---|---|---|---|
| Local | The current function or other current local context | A parameter or name assigned in the function | First, for code running inside a function |
| Enclosing | Outer function scopes around a nested function | A name in a factory function | After Local, when functions are nested |
| Global | The current module's top-level namespace | A constant assigned outside functions | After Local and Enclosing |
| Built-in | Names supplied by Python | len, print, and range | Last, if no program-defined binding was found |
label = "global"
def outer():
label = "enclosing"
def inner():
local_value = "local"
print(local_value) # Local
print(label) # Enclosing
print(len("abc")) # len is Built-in
inner()
outer()
If inner had no local or enclosing label, Python would try the module-level label. If no matching name existed at any level, the result would be NameError.
Reading Versus Assigning Names
Reading a global name inside a function is allowed:
tax_rate = 0.2
def add_tax(price):
return price * (1 + tax_rate)
print(add_tax(100))
However, an assignment anywhere in a function causes Python to classify that name as local throughout that function, unless global or nonlocal is declared. This classification happens even if the assignment appears after a read.
value = 10
def show_value():
print(value) # UnboundLocalError
value = 20
show_value()
Python sees the assignment to value and treats every use of value in that function as a local use. The read occurs before the local binding has received a value, so Python raises UnboundLocalError, a specialized form of NameError.
The global Statement
global tells Python that assignments to a name inside a function should target the current module's top-level binding rather than create a local binding.
counter = 0
def next_counter():
global counter
counter += 1
return counter
print(next_counter()) # 1
print(next_counter()) # 2
print(counter) # 2
Without global counter, the augmented assignment would be treated as a local assignment and would fail because the local counter had not been initialized.
Unrestricted global mutation makes code harder to test and reason about: any function may change shared state, and call order can matter. Prefer arguments and return values in ordinary application code:
def next_counter(counter):
return counter + 1
counter = 0
counter = next_counter(counter)
Use global only when deliberate, limited module-level state is the clearest design.
The nonlocal Statement, Enclosing Scope, and Closures
An enclosing scope belongs to an outer function surrounding a nested function. nonlocal makes assignment in the nested function target a name in the nearest enclosing function scope.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
The returned nested function is a closure: it retains access to count after make_counter has returned. nonlocal cannot target a module-level global name. It also requires an existing binding in an enclosing function scope; otherwise Python raises a syntax error.
Mutation Versus Rebinding
Rebinding makes a name refer to a different object. Mutation changes the contents of an existing mutable object while preserving the name's binding.
items = ["book"]
def append_item():
items.append("pen") # mutation; no global needed
def replace_items():
items = ["new list"] # new local binding
append_item()
print(items) # ['book', 'pen']
replace_items()
print(items) # ['book', 'pen']
The function can mutate the list because it does not assign a new object to the name items. If it must replace the outer list, it needs global items for a module-level list or nonlocal items for an enclosing-function list.
| Operation inside a function | Example target | Scope declaration needed | Effect on outer value |
|---|---|---|---|
| Read global name | print(total) | No | Reads the module-level binding |
| Reassign global name | total = 0 | global total | Replaces the module-level binding |
| Mutate global list or dictionary | items.append(x) | No, if the name is not reassigned | Changes the shared object |
| Reassign enclosing name | count = count + 1 | nonlocal count | Replaces the outer-function binding |
| Mutate an enclosing mutable object | state["ready"] = True | No, if the name is not reassigned | Changes the shared object |
An augmented assignment such as items += [x] is commonly treated as assignment for scope analysis. In a function, it can therefore require global items or nonlocal items, even though the operation may mutate a list in place at runtime.
Scope Boundaries and Modern Python Details
- Functions and lambdas: each function call has local names. A lambda follows the same general lexical scope rules as a function.
- Modules: a module has its own top-level namespace. Its global names are not automatically shared with unrelated modules.
if,for, andwhile: these blocks do not create a separate local scope. A name assigned in one of these blocks remains available in the surrounding function or module scope, subject to normal execution rules.- Comprehensions: in Python 3, a comprehension has its own iteration-variable scope. The loop variable does not leak into the surrounding scope.
- Classes: a class body creates a class namespace, but class name lookup has special behavior. A class body should not be treated exactly like a nested function's enclosing scope. Methods do not automatically use class-body names as ordinary enclosing locals; use an instance, class, or module reference as appropriate.
numbers = [1, 2, 3]
squares = [number * number for number in numbers]
# Python 3: this raises NameError because number is
# local to the comprehension.
print(number)
Good Scope Design Practices
- Use function parameters for inputs and return values for outputs.
- Keep state explicit where possible instead of relying on hidden global or enclosing mutation.
- Use descriptive names to reduce accidental shadowing.
- Do not overwrite built-in names such as
list,str,id, ormax. - Reserve
globalandnonlocalfor clear, limited cases where shared state is intentional. - Copy a mutable object before modifying it when a function should not affect its caller's object.
def add_tag(tags, tag):
new_tags = tags.copy()
new_tags.append(tag)
return new_tags
original = ["python"]
updated = add_tag(original, "scope")
print(original) # ['python']
print(updated) # ['python', 'scope']
Common Scope Errors and Fixes
| Symptom | Likely cause | Typical exception | Correction |
|---|---|---|---|
| Accessing another function's local variable | The name belongs only to the first function's call | NameError | Pass it as an argument, return it, or deliberately place state in an outer scope |
| Using a name absent from every lookup scope | No Local, Enclosing, Global, or Built-in binding exists | NameError | Define the name, correct its spelling, or pass the required value |
| Reading a local name before assignment | An assignment anywhere classified the name as local | UnboundLocalError | Rename the local, initialize it first, pass and return values, or use a suitable declaration |
| Unexpectedly changing shared mutable state | A list or dictionary was mutated through a shared reference | Usually no exception | Copy the object for isolation, or document and intentionally manage the mutation |
Diagnosing Typical Problems
- If a second function receives
NameErrorfor a value created by the first, the value is local to the first function. Pass it, return it, or choose an intentionally shared scope. - If a global value appears unchanged after a function assigns the same spelling, the assignment probably created a local shadow. Return the new value and assign it at the caller, or use
globalonly when appropriate. - If a nested function cannot update an outer value, assignment created a new local name. Declare
nonlocalfor an existing enclosing binding, or return the updated value instead. - If assigning to
listorstrcauses confusing failures, a built-in was shadowed. Rename the variable and restart the interactive session if the bad binding remains active.
Exam- and Debugging-Relevant Rules
- LEGB means Local, Enclosing, Global, Built-in.
- Reading an accessible global does not require
global; rebinding it does. - An assignment anywhere in a function makes that name local throughout the function unless declared
globalornonlocal. globaltargets the current module;nonlocaltargets the nearest enclosing function scope.- Mutation of a shared list or dictionary is different from rebinding the name that refers to it.
NameErrormeans lookup found no accessible binding.UnboundLocalErrormeans Python expected a local binding, but it was read before assignment.
For related practice, review assignment operators, importing modules, Python lists, Python dictionaries, and types of Python errors.