Python Variable Scope: LEGB, global, and nonlocal
python variable scope: Learn how Python resolves names with the LEGB rule, and how to use global and nonlocal to modify variables in outer scopes.
Python's variable scope determines where a name is visible and how it is resolved. The rules are simple once you understand the LEGB lookup order, but they cause confusion in nested functions, closures, and loops. This article explains the mechanics of python variable scope and shows how to use global and nonlocal correctly.
The LEGB Rule and How Python Resolves Names
When Python encounters a name in an expression, it searches for that name in a specific order: Local, Enclosing, Global, Built-in. This is the LEGB rule. The search stops at the first scope that contains the name. If no match is found, a NameError is raised.
- Local: names assigned within the current function.
- Enclosing: names in the local scope of any enclosing function, from inner to outer.
- Global: names assigned at the top level of a module.
- Built-in: names pre-defined in the
builtinsmodule, likelenorprint.
Consider this example:
x = "global" def outer(): x = "enclosing" def inner(): x = "local" print(x) inner() outer() # prints "local"
The print(x) inside inner finds x in the local scope of inner. If that assignment were removed, it would find the enclosing x, then the global x. Understanding this order is the foundation for working with scope in Python.
Local Scope and Function Parameters
Every function call creates a new local scope. Parameters are local names that are bound to the arguments passed in. Assignments inside the function create local names unless explicitly declared otherwise.
def add(a, b): result = a + b return result
Here a, b, and result are all local to add. They are not accessible outside the function. This isolation is useful because it prevents accidental interference between different parts of a program.
A common mistake is trying to modify a global variable inside a function without declaring it. For example:
counter = 0 def increment(): counter += 1 # UnboundLocalError
This raises UnboundLocalError because the assignment makes counter local, but the += operation reads it before it is assigned. The fix is to use the global keyword.
Using global to Modify Module-Level Variables
To modify a variable that lives at the module level, you must declare it as global inside the function. This tells Python that the name refers to the global binding, not a new local one.
counter = 0 def increment(): global counter counter += 1 increment() print(counter) # 1
Without global, the assignment counter += 1 creates a new local variable and leaves the global untouched. The global statement must appear before any use of the variable in the function. It applies to the entire function body.
Use global sparingly. Overusing it makes functions dependent on external state, which complicates testing and debugging. A cleaner approach is often to return the new value and let the caller assign it.
Using nonlocal in Nested Functions
When you have a nested function and want to modify a variable in the enclosing function's scope, global is not the right tool. global always refers to the module-level scope. For an enclosing function's local variable, use nonlocal.
def outer(): count = 0 def inner(): nonlocal count count += 1 inner() return count print(outer()) # 1
nonlocal binds the name to the nearest enclosing scope that already defines it. It cannot be used at the module level. If the variable does not exist in an enclosing scope, a SyntaxError is raised.
This is essential for closures that maintain state across calls, such as counters or accumulators.
Closures and Late Binding in Loops
A closure is a function that captures variables from its enclosing scope. A classic pitfall involves creating closures inside a loop. The captured variable is looked up at call time, not at creation time, leading to unexpected behavior.
funcs = [] for i in range(3): funcs.append(lambda: i) for f in funcs: print(f()) # prints 2, 2, 2
All three lambdas reference the same loop variable i, which ends with value 2. To capture the current value, use a default argument or a factory function:
funcs = [] for i in range(3): funcs.append(lambda i=i: i) for f in funcs: print(f()) # prints 0, 1, 2
The default argument binds the value at definition time, creating a separate local scope for each lambda.
Scope, Maintainability, and Debugging
Variable scope directly affects code maintainability. Functions that rely on global variables are harder to reason about because their behavior depends on external state that may change elsewhere. Prefer passing values as arguments and returning results. This makes functions pure and testable.
Scope also influences debugging. A NameError or UnboundLocalError often points to a misunderstanding of where a name is defined. Reading the traceback and identifying which scope is being searched can save time. Use tools like dis to inspect bytecode if you need to verify how names are resolved.
For performance, scope lookup is fast, but accessing globals is slightly slower than locals because of dictionary lookups. In hot loops, assigning a global to a local variable can reduce overhead. This is a micro-optimization and rarely necessary unless profiling shows it matters.
Common Scope-Related Errors and Their Fixes
The most frequent errors are UnboundLocalError and NameError. UnboundLocalError occurs when a name is assigned in a function but read before the assignment. NameError occurs when no scope contains the name.
Another common issue is using global when nonlocal is needed, or vice versa. The rule of thumb: use global for module-level variables, nonlocal for variables in an enclosing function.
Consider this bug:
def make_counter(): value = 0 def increment(): value += 1 # UnboundLocalError return value return increment
Because value is assigned in increment, it becomes local. Adding nonlocal value fixes it. The same pattern applies when you need to update a mutable object like a list or dict; you can modify the object without nonlocal, but reassigning the variable requires it.
Understanding these distinctions helps you write predictable code and diagnose scope-related bugs quickly.