Home/Learn/Python A–Z/Scope, LEGB Rule & Closures

Scope, LEGB Rule & Closures

Intermediate
Functions

Python resolves names through LEGB — Local, Enclosing, Global, Built-in — and inner functions capture enclosing variables as closures, the mechanism behind decorators.

Overview

When Python sees a name, it searches four layers in order: Local (current function), Enclosing (any outer function), Global (module), Built-ins (print, len...). Assignment inside a function creates a LOCAL name by default — even if a global with the same name exists — which produces the famous UnboundLocalError surprise. Closures are inner functions that remember variables from their enclosing scope even after the outer function has returned; they power decorators, callbacks and function factories. global and nonlocal exist to rebind outward — use them rarely.

LEGB & the UnboundLocalError Trap

Reading a global inside a function works. ASSIGNING to that name anywhere in the function makes it local for the WHOLE function — including lines before the assignment.

Why count += 1 explodes without global
count = 0                # global

def show():
    print(count)         # OK — reads global (L? no, E? no, G? yes)

def bump_broken():
    count += 1           # UnboundLocalError!
    # assignment makes count LOCAL for the whole function,
    # so count += 1 reads a local that doesn't exist yet

def bump():
    global count         # explicitly rebind the global
    count += 1

bump(); bump()
print(count)             # 2

# Prefer: pass values in, return values out — avoid global state
def bump_pure(c):
    return c + 1

Closures & nonlocal

The inner function keeps a live reference to the enclosing variable (a cell), not a copy. nonlocal lets the inner function REBIND the enclosing name — that's how stateful closures like counters work.

Closures = functions with memory
def make_counter():
    count = 0
    def increment():
        nonlocal count      # rebind enclosing, not global
        count += 1
        return count
    return increment

c1 = make_counter()
c2 = make_counter()          # independent state!
print(c1(), c1(), c1())      # 1 2 3
print(c2())                  # 1

# Function factory — closure captures rate
def make_gst(rate):
    def apply(amount):
        return amount * (1 + rate / 100)
    return apply

gst18 = make_gst(18)
print(gst18(1000))           # 1180.0

print(c1.__closure__[0].cell_contents)  # 3 — the captured cell, visible!

Key Points to Remember

  • 1Name lookup order: Local → Enclosing → Global → Built-in (LEGB)
  • 2Assignment anywhere in a function makes that name local everywhere in it — cause of UnboundLocalError
  • 3Closures capture variables by reference (cells), not by value
  • 4nonlocal rebinds an enclosing variable; global rebinds a module variable — both are code smells if frequent

Interview Questions

Sign in to ask Aria
1

Explain the LEGB rule with an example where each level wins.

MediumTransUnion
2

What is a closure? Build a counter without classes and explain why the state survives.

MediumCRED
3

Why does x += 1 inside a function raise UnboundLocalError when x is global?

HardGoogle

Ask Aria about Scope, LEGB Rule & Closures

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…