Skip to content

Python · Fundamentals

Python Scope and the LEGB Rule Explained — global, nonlocal, and Closures

4 min read Updated 2026-06-19 Share:

Practice Variables, Scope & the LEGB Rule interview questions

How Python finds your variables

When you write a name like x, Python has to decide which x you mean. The rule it uses is called LEGB, and almost every confusing scope bug — UnboundLocalError, closures that "capture the wrong value", global/nonlocal mix-ups — comes from misunderstanding it. This guide walks through name resolution from the ground up.

The LEGB rule

To resolve a name being read, Python searches four scopes in order, stopping at the first match:

  • Local — names assigned inside the current function.
  • Enclosing — names in any enclosing function (for nested functions).
  • Global — names at the top level of the module.
  • Built-in — names like len, print, range.
x = "global"

def outer():
    x = "enclosing"
    def inner():
        print(x)     # finds "enclosing" — the nearest scope that has x
    inner()

outer()              # enclosing

If inner had its own x, that local would win. If no scope has the name, you get a NameError.

Assignment decides the scope

Here's the rule that catches everyone: assigning to a name anywhere in a function makes that name local for the entire function — even before the assignment line runs.

count = 0

def increment():
    count = count + 1    # UnboundLocalError!
    return count

Because count is assigned in increment, Python treats it as a local throughout the function. The right-hand side then tries to read a local that hasn't been assigned yet — so it raises UnboundLocalError, not a NameError and not "use the global one".

The global keyword

To rebind a module-level name from inside a function, declare it global:

count = 0

def increment():
    global count
    count = count + 1    # now refers to the module-level count
    return count

increment()              # 1
increment()              # 2

Note you only need global to assign. Reading a global needs no declaration — mutating a global list (my_list.append(...)) also needs none, because that's mutation, not rebinding.

The nonlocal keyword

nonlocal is the enclosing-scope equivalent: it lets a nested function rebind a variable in the nearest enclosing function (not the module).

def make_counter():
    count = 0
    def increment():
        nonlocal count        # rebind the enclosing count
        count += 1
        return count
    return increment

c = make_counter()
c(), c(), c()                 # (1, 2, 3)

Without nonlocal, count += 1 would make count local to increment and raise UnboundLocalError. nonlocal requires an existing binding in an enclosing function — it can't reach the global scope and it can't create a new variable.

The late-binding closure trap

Closures capture variables, not values — they look the variable up when called, not when defined. This bites in loops:

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]          # [2, 2, 2] — not [0, 1, 2]!

All three lambdas share the same i, which is 2 by the time they run. The fix is to bind the current value with a default argument (evaluated at definition time):

funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]          # [0, 1, 2]

Comprehensions have their own scope

Since Python 3, comprehensions run in their own scope, so the loop variable doesn't leak into the surrounding function:

[y for y in range(3)]
y                              # NameError — y did not leak out

This is different from a plain for loop, where the loop variable does survive after the loop ends.

Recap

Python resolves names with LEGB: Local, Enclosing, Global, Built-in, stopping at the first match. Assignment anywhere in a function makes the name local for the whole function, which is the source of UnboundLocalError. Use global to rebind a module-level name and nonlocal to rebind an enclosing function's name — both are only needed for assignment, not for reading or mutating. Remember that closures capture variables (late binding), so capture per-iteration values with a default argument when you build functions in a loop.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel