PythonVariable Scope (LEGB)

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, like len, 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

Python
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 above
local x
global x

Now remove the local x from inner() and watch Python fall back to Enclosing:

Python
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:

Python
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.

Python
print(len("hello"))  # both 'print' and 'len' resolve via Built-in scope
5
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.

Python
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)  # 2
2

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.

Python
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())  # 3
1
2
3
Note
Without `nonlocal`, `count += 1` inside `increment` would raise `UnboundLocalError` for the same reason as the `global` example above — assignment makes Python treat `count` as local unless told otherwise.
Shadowing Built-in Names
Warning
Avoid naming variables `list`, `str`, `dict`, `id`, `type`, `sum`, `input`, or other built-in names. Doing so *shadows* the built-in within that scope, silently breaking any code after it that expected the real built-in to still be available.

Python
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)

nonlocal

Global

Top level of the module

global

Built-in

Provided by Python itself

n/a (avoid shadowing instead)

Example
A nested function structure showing all four LEGB levels resolving in the same program.

Python
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
Tip
As a rule of thumb, keep functions small and avoid relying on `global`/`nonlocal` wherever possible — passing values in as parameters and returning results explicitly makes code far easier to reason about and test.