Python/Functions/Scope & the LEGB Rule

Scope & the LEGB Rule

Scope is the region of code where a name is visible, and the LEGB rule (Local, Enclosing, Global, Built-in) is the fixed order Python searches to resolve every name you use.

Intermediate30mInterview Production

Learning objectives

  • Resolve any name by applying the LEGB rule in the correct order
  • Distinguish the local, enclosing, global and built-in namespaces
  • Use `global` and `nonlocal` correctly to rebind outer names
  • Diagnose and fix UnboundLocalError, leaking variables and shadowed built-ins

Prerequisites

Variables & ReferencesFunctionsNested functions

Version notes

  • · In Python 3 a comprehension or generator expression runs in its own scope, so its loop variable no longer leaks into the surrounding code (it did leak in Python 2).
  • · `nonlocal` was added in Python 3.0; in Python 2 there is no supported way to rebind an enclosing-scope variable.
  • · Python 3.11+ raises a clearer message: "cannot access local variable 'x' where it is not associated with a value" (earlier versions said "local variable 'x' referenced before assignment").

Simple explanation

What it is. Scope is the part of a program where a name can be used without qualification. Every time Python meets a bare name it searches up to four namespaces in a fixed order — Local (the current function), Enclosing (any outer functions), Global (the module), and Built-in (Python's own names such as len and print). The first namespace that holds the name wins; if none does, you get a NameError.

Why it exists. A real program has thousands of names and many of them repeat. Scope rules let the same name mean different things in different places without collisions, and they let each function use short local names without worrying about the rest of the codebase. LEGB is simply the deterministic order Python uses so that lookups are predictable rather than guesswork.

The problem it solves. Getting scope wrong is one of the most common sources of surprising bugs: functions that silently read stale global state, the UnboundLocalError that appears the moment you assign to a name you also read, and 'my loop variable overwrote something' mistakes. Knowing LEGB turns those mysteries into rules you can apply on sight.

How it works. Python decides each name's scope at compile time, before the function ever runs. When it compiles a function it scans the whole body: any name assigned anywhere in the function (via =, for, def, import, with ... as, etc.) is marked local for the entire body — even on lines above the assignment. Reads then follow LEGB. The `global` and `nonlocal` statements override this classification so that an assignment targets the module or the enclosing scope instead of creating a new local.

Analogy: Think of looking for a book. You first check the desk right in front of you (Local), then the shelves in your own office (Enclosing), then the building's main library (Global), and finally the national archive everyone shares (Built-in). You stop at the first copy you find — and if you keep your own copy on your desk, you never even notice the one in the library.

Common misconceptions

  • "You need the `global` keyword to read a global inside a function." — No. You can read a global freely; you only need `global` to *reassign* it (or `nonlocal` for an enclosing name).
  • "A name becomes local only from the line where you assign it." — It is local for the *entire* function body. That is exactly why a read placed above the assignment raises UnboundLocalError instead of falling back to the global.
  • "A comprehension's loop variable leaks into the surrounding scope." — A plain `for`-loop variable does leak (it is just a normal local assignment), but in Python 3 a comprehension or generator expression has its own private scope, so its variable does not.
  • "An inner function can change an enclosing variable just by assigning to it." — Assigning creates a new local that shadows the outer name; you must declare `nonlocal` to rebind the enclosing one.

Common alternatives

  • Pass data in as parameters and return results, instead of reading or mutating module-level globals.
  • Group shared mutable state on a class or dataclass instance rather than in loose global variables.
  • Use a small `nonlocal` closure for private state, and an explicit configuration object for anything larger.

Interactive visualization

Step 1 / 4
1x = "global"
2
3def outer():
4 x = "enclosing"
5 def inner():
6 print(x) # LEGB search for x
7 inner()
8
9outer()

Call stack

<module>

x = "global"

Module level defines a global x

The name x is bound to "global" in the module (global) namespace — the G in LEGB.

Code examples

BeginnerThe four levels of LEGB
python
Loading editor…

Note: Each `print` resolves `x` from a different level. Notice that `inner`'s local `x` never touches `outer`'s, and `len` is found only after Local, Enclosing and Global have all missed.

Behind the scenes

  • Scope is resolved at compile time, not run time. When CPython compiles a function it builds a symbol table that classifies every name as local, free (from an enclosing scope), global, or a cell.
  • That classification chooses the bytecode: LOAD_FAST for locals (a fast array slot), LOAD_DEREF for enclosing/cell variables, LOAD_GLOBAL for module and built-in names, and LOAD_NAME for the dynamic module top level.
  • Local names live in an array indexed by position (see `func.__code__.co_varnames`), which is why local access is measurably faster than the dictionary lookups used for globals.
  • Because an assignment anywhere in the body marks a name local for the whole function, a read compiled to LOAD_FAST can run before its slot is filled — that empty slot is exactly what raises UnboundLocalError.
  • The built-in namespace is just the `builtins` module. A global name shadows it, so defining your own `list` or `id` hides the real one until you `del` your version or restart the interpreter.

Interview insights

Key definitions

  • · Scope: the textual region of a program where a name is directly accessible without qualification.
  • · Namespace: a mapping from names to objects, such as a module's globals dict or a frame's locals.
  • · LEGB: the order Python searches namespaces to resolve a name — Local, Enclosing, Global, Built-in.
  • · Free variable: a name used in a function that is defined in an enclosing function's scope.
What is the LEGB rule and how does Python use it to resolve a name?

Concise answer: LEGB is the order Python searches for a name: Local, then any Enclosing functions, then the module Global scope, then Built-ins. It stops at the first match and raises NameError if none has it.

Detailed answer: When Python evaluates a bare name it looks in the local namespace of the current function first, then in the namespaces of any enclosing functions (nearest first), then in the module's global namespace, and finally in the built-in namespace. The search is read-only and stops at the first hit, which is why a local name shadows a global of the same name. Whether a name counts as local is decided at compile time by scanning the body for assignments — not by the order in which lines execute.

Common mistake: Assuming the search includes the *caller's* locals. Python scoping is lexical (based on where code is written), not dynamic (based on who called the function).

Follow-ups:

  • · Where do comprehension loop variables fit into LEGB?
  • · Is a name's scope decided at run time or compile time?
Why does assigning to a variable you also read inside a function raise UnboundLocalError?

Concise answer: Assigning to a name anywhere in a function makes it local for the entire body. The read happens before that local has a value, so there is nothing to load — and Python does not fall back to the global.

Detailed answer: Python classifies names at compile time. If a function assigns to `count` anywhere, `count` is local throughout the function, including lines above the assignment. Reading it first therefore hits an unassigned local slot and raises UnboundLocalError instead of reading the global `count`. The fixes are: declare `global count` (or `nonlocal count` for an enclosing variable) so the assignment targets the outer name, use a different local name, or pass the value in as a parameter and return the result.

Common mistake: Thinking the read will 'see' the global because the assignment sits on a later line. Line order is irrelevant; the whole body is scanned first.

Follow-ups:

  • · How would you fix it without using `global`?
  • · Does augmented assignment like `count += 1` trigger this too? (Yes — it is both a read and a write.)
What is the difference between `global` and `nonlocal`?

Concise answer: `global x` makes assignments target the module-level name; `nonlocal x` makes them target the nearest enclosing *function* scope. Neither keyword is needed just to read a name.

Detailed answer: Both statements change where an assignment binds. `global` binds to the module namespace, creating the name there if it does not exist. `nonlocal` binds to the nearest enclosing function scope and requires that the name already exist there — otherwise it is a compile-time error, and it never reaches the global scope. You only need either keyword when you *reassign* a name (including `+=`); reading an outer name works fine without them.

Common mistake: Reaching for `global` inside a nested function when you meant `nonlocal`, which skips the enclosing scope and touches the module instead (or fails to find the name at all).

Follow-ups:

  • · What happens if you use `nonlocal` for a name that does not exist in any enclosing scope?
  • · Can `nonlocal` reach a variable two levels up? (Yes — the nearest enclosing scope that defines it.)

Knowledge check

What's the output?Question 1 of 3

What is the result of running this code?

python
x = 5
def f():
    print(x)
    x = 10
f()
What's the output?Question 2 of 3

What does this print in Python 3?

python
i = 'outer'
result = [i for i in range(3)]
print(i)
Multiple choiceQuestion 3 of 3

In what order does Python search namespaces to resolve a name?

Score: 0/3