LEGB rule: variable scope in Python

RoadmapsPython

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

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

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

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

Return to Python Roadmap