Variables, Scope & the LEGB Rule Interview Questions & Answers

5 questions Updated 2026-06-18

Python interview questions on the LEGB scope rule, global vs nonlocal, UnboundLocalError, late binding in loop closures, and name shadowing.

LEGB describes the order Python searches for a name: Local (inside the current function), Enclosing (any outer functions), Global (the module's top level), then Built-in (names like len, print). The first match wins, and the search stops there.

x = "global"
def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)     # "local"  — Local found first
    inner()
outer()

Why it matters: nearly every "why is this variable that value?" question reduces to walking L -> E -> G -> B until a name is found.

Both let you rebind a name from an outer scope instead of creating a new local. global targets the module-level name; nonlocal targets the nearest enclosing function scope (and that name must already exist there).

count = 0
def inc():
    global count
    count += 1        # rebinds module-level count

def outer():
    x = 1
    def inner():
        nonlocal x
        x = 2          # rebinds outer's x, not a new local
    inner()
    return x           # 2

Rule of thumb: you only need these keywords to reassign an outer name — you can always mutate an outer mutable object (e.g. list.append) without them.

Python decides a name's scope at compile time by scanning the whole function body. If a name is assigned anywhere in a function, it is treated as local for the entire function — even on lines before the assignment. Reading it before it's bound raises UnboundLocalError.

x = 10
def f():
    print(x)      # UnboundLocalError: x is local because of the line below
    x = 20        # this assignment makes x local everywhere in f

The fix is to declare global x (or nonlocal x) if you meant the outer name, or simply read a different name. Rule of thumb: an assignment anywhere makes the name local everywhere in that function.

Closures capture variables, not values — this is late binding. The inner function looks up the loop variable when it is called, by which time the loop has finished and the variable holds its final value.

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]      # [2, 2, 2]  — all see the final i

# Fix: bind the current value via a default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]      # [0, 1, 2]

The default-argument trick captures i's value at definition time. Rule of thumb: if loop-created closures behave strangely, you're hitting late binding — bind the value explicitly.

A local name shadows (hides) an outer name of the same identity for the duration of the scope. Assigning to it inside a function creates a separate local that leaves the module-level name untouched.

value = "module"
def f():
    value = "function"   # new local — shadows the global
    print(value)         # "function"
f()
print(value)             # "module"  — unchanged

list = [1, 2]            # shadows the built-in list() in this scope!

Watch out for shadowing built-ins (list, id, sum, type) — it silently breaks later calls. Rule of thumb: keep names distinct from outer scopes and built-ins to avoid surprising lookups.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.