LEGB rule: variable scope in Python
Overview
When Python encounters a name, it searches four scopes in order. This is known as the `LEGB` rule. Think of scope like searching for a tool: first you check your pockets (Local), then your toolbox (Enclosing), then the garage (Global), and finally the hardware store (Built-in). - `L` (Local): names defined inside the current function - `E` (Enclosing): names in any surrounding (outer) function - `G` (Global): names defined at the module (file) level - `B` (Built-in): Python's built-in names like `len`, `print`, `range` Python uses the first match it finds. If no match is found, a `NameError` is raised. x = 'global' def outer(): x = 'enclosing' def inner(): print(x) inner() outer() print(x) To modify a global variable from inside a function, declare it with `global x`. To modify a variable in an enclosing function's scope, use `nonlocal x`. Without these keywords, an assignment inside a function always creates a new local variable. funcs = [lambda: i for i in range(3)] print([f() for f in funcs]) # Prints [2, 2, 2], not [0, 1, 2] This is the late-binding closure trap. Python looks up `i` in the enclosing scope when the function is called, not when it is defined. This is covered fully in `lambda-functions`.
`LEGB` explains every `NameError` and `UnboundLocalError` you will encounter. It also explains why global state in a module works across functions without passing arguments, a pattern common in config objects and connection pools.
Where used: Module-level config objects accessed from route handlers., Counter variables in enclosing scopes (used by closures, which are functions that remember the variables from their enclosing scope)., Built-in shadowing bugs (e.g., naming a variable `list` or `id`).
Why learn this
- Diagnosing `UnboundLocalError`, the most common scoping bug. This is caused by assigning to a name that Python then treats as local throughout the whole function.
- Understanding how closures retain enclosing-scope values. This forms the foundation for decorators and factory functions.
Code walkthrough
x = 'global'
def outer():
x = 'enclosing'
def inner():
print(x)
inner()
outer()
Focus: Notice how `inner` prints `'enclosing'` instead of `'global'` because `LEGB` stops at the enclosing scope first.
Common mistakes
- `UnboundLocalError` from a local assignment after a read: If you assign to a name anywhere in a function (e.g., `count = 1`), Python marks it local for the entire function. Reading it before the assignment raises `UnboundLocalError`. Fix this by declaring `global count` or `nonlocal count`, or restructure to pass and return the value.
- Shadowing a built-in name: Assigning to `list = [1, 2]` or `id = 42` at the module level replaces the built-in for the rest of the file. Always use descriptive names like `user_list` or `record_id` instead.
- Expecting an assignment inside a function to change the global: Without the `global x` declaration, `x = 'new'` inside a function creates a new local `x`. The module-level `x` remains completely unchanged.
- Late-binding closures in loops: Functions created inside a loop (like `lambda`s) do not capture the value of the loop variable at creation time. They capture a reference to the variable, which will have its final value when the function is later called.
Glossary
- LEGB rule
- The specific order (`Local`, `Enclosing`, `Global`, `Built-in`) Python follows to look up the value of a variable. Example: Python checks `Local` before `Global`.
- enclosing function
- An outer function that contains an inner, nested function within its code. Example: `def outer(): def inner(): pass`.
Recall questions
- What order does Python search for a name, from innermost to outermost?
- What keyword is required to modify a module-level variable from inside a function?
- What causes `UnboundLocalError`?
- What keyword lets you modify a variable in an enclosing (outer) function's scope?
- Why do functions created in a loop often exhibit unexpected behavior if they reference a loop variable?
Understanding checks
What does this code print?
enclosing
Python searches scopes in `LEGB` order. For the `x` inside `inner()`, it doesn't find it in Local, so it moves to Enclosing (the `outer` function) where it finds `x = 'enclosing'`.
Why does the following code raise an `UnboundLocalError`? count = 10 def increment(): print(count) count = 20 increment()
Because assigning to `count` inside the function makes it a local variable for the entire function.
Any assignment to a variable inside a function without a `global` or `nonlocal` declaration causes Python to treat it as a local variable everywhere in that function. The `print(count)` tries to read this local variable before the assignment has happened.
Practice tasks
Fix the request counter
The `increment` function currently throws an error because it tries to modify the module-level `_count` variable without declaring it. Modify the function so that it correctly updates the global `_count`.
Challenge
Rate limiter counter
Write `make_counter(limit)` that returns two functions: `tick()` and `reset()`. `tick()` increments an internal count and returns `'ok'` if `count <= limit`, or `'limit reached'` otherwise. `reset()` sets `count` back to 0. Use `nonlocal` to share the count between the two returned functions. Test with `limit=2`: call `tick()` three times, print results, then `reset()` and `tick()` once more.
Continue learning
Previous: Defining functions and return values
Previous: Primitive types: int, float, bool, str
Next: Lambda functions and when to use them
Next: First-class functions: passing and returning functions