1.2.30 · D5Introduction to Programming (Python)
Question bank — Variable scope — LEGB rule (Local, Enclosing, Global, Built-in)
True or false — justify
Every answer must say why, not just T/F.
Reading a global variable from inside a function requires the global keyword
False. Reading searches outward (L→E→G→B), so a global is visible for free. You only need
global to rebind (write to) it.global x creates the global variable x if it doesn't exist yet
True (kind of). If
x is not yet in the module namespace, assigning to it after global x creates it there. Unlike nonlocal, global does not require the name to pre-exist.nonlocal x works even when there is no enclosing function that assigns x
False.
nonlocal binds to the nearest enclosing function's x, so that x must already exist in an enclosing scope, or you get a SyntaxError at compile time.A for loop introduces a new scope for its loop variable
False. In Python only functions, modules, and comprehensions create scope. The loop variable leaks into the surrounding function/module and survives after the loop ends.
The name len cannot be reassigned because it is built-in
False. You can write
len = 5 at module level; that shadows the built-in in G, and later calls to len(...) fail because G is searched before B.If a function only reads count (never assigns it), Python looks up count via LEGB
True. With no assignment to
count anywhere in the function body, count is not local, so Python searches L→E→G→B normally.Adding a single line count = 5 at the bottom of a function changes how count is read at the top
True. Python scans the whole function at compile time; one assignment anywhere marks the name Local for the entire body, so an earlier read becomes an
UnboundLocalError.A comprehension like [i for i in range(3)] leaks i into the surrounding scope
False (Python 3). Comprehensions have their own scope, so
i does not survive. This differs from a plain for loop, which does leak.Two functions can each have their own local x without interfering
True. Each function call gets its own local namespace, so
x in one is a completely different name-binding from x in the other — that is the whole point of scope.global and nonlocal are interchangeable ways to edit an outer variable
False.
global targets the module scope (G); nonlocal targets the nearest enclosing function scope (E). They point at different rooms.Spot the error
Read the snippet, name the exact failure and why.
x = 10
def f():
print(x)
x = 20
f()
``` ::: **`UnboundLocalError`.** The assignment `x = 20` makes `x` Local for all of `f`, so the earlier `print(x)` reads a local that has no value yet — the global `x = 10` is never consulted.
```python
def outer():
def inner():
nonlocal n
n = 5
inner()
outer()
``` ::: **`SyntaxError` (no binding for nonlocal `n`).** `nonlocal n` needs an `n` that already exists in an enclosing function, but `outer` never defines one. Enclosing scope must supply the name first.
```python
def bump():
total += 1
total = 0
bump()
``` ::: **`UnboundLocalError`.** `total += 1` is a read-then-write, so `total` is Local; reading it before assignment fails. Fix with `global total` inside `bump`.
```python
print = "hello"
print("done")
``` ::: **`TypeError: 'str' object is not callable`.** Assigning to `print` shadows the built-in in **G**; the call now tries to call a string. No error until you *call* it.
```python
def f():
global y
return y
f()
``` ::: **`NameError: name 'y' is not defined`.** `global y` says "use the module-level `y`", but no `y` exists at module level and none is assigned here, so the read finds nothing.
```python
def counter():
def step():
count = count + 1
return count
count = 0
return step()
counter()
``` ::: **`UnboundLocalError` inside `step`.** `count = count + 1` makes `count` local to `step`; `step` never sees `outer`'s `count`. Needs `nonlocal count` to reach the enclosing scope.
---
## Why questions
The point is the *reasoning*, so answers are a sentence or two.
Why does Python decide a name is local at *compile* time, not while running? ::: Because it scans each function body once up front for any assignment target; this lets it allocate fast local-variable slots instead of dictionary lookups, at the cost of the "assignment anywhere makes it local everywhere" rule.
Why is `UnboundLocalError` a *different* error from `NameError`? ::: `NameError` means the name was never found in any scope; `UnboundLocalError` means Python *knows* the name is Local (an assignment marks it so) but you read it before it got a value. The name exists as a slot but is empty.
Why can you `list.append(item)` inside a function without `global`, but not `list = list + [item]`? ::: `append` *mutates* the existing object the name already points to — no name is rebound, so no scope rule fires. `list = ...` *rebinds* the name, which triggers the local-creation rule.
Why does `len("hi")` work with no import while `sqrt(4)` does not? ::: `len` lives in the **Built-in (B)** scope that LEGB always searches last, so it is always found. `sqrt` is not built-in; it lives in the `math` module and must be imported into **G** first.
Why does shadowing a built-in only cause trouble *later*, not at the assignment line? ::: The assignment `len = 5` simply binds a name in **G** — perfectly legal. The clash only surfaces when code later tries to *use* `len` expecting the original built-in behaviour.
Why do closures need the **Enclosing (E)** scope specifically, not Global? ::: Each call to the outer function creates a *fresh* enclosing namespace, so each closure can capture its own private state (like a per-call counter) that no other call or the module can see.
Why doesn't an `if` block get its own scope like it does in C or Java? ::: Python scopes by *function/module/comprehension boundaries*, not by `{}` blocks. A name assigned inside `if` simply belongs to the surrounding function or module, so it stays visible after the `if`.
---
## Edge cases
Boundary and degenerate situations the rules must still cover.
A name is assigned *and* read only inside one branch of an `if` that never runs — is it Local? ::: **Yes, Local.** Locality is decided by the *presence* of an assignment statement in the source, not by whether that statement executes. Reading it elsewhere may still `UnboundLocalError`.
A function with `global x` but `x` also passed as a parameter named `x` — legal? ::: **No, `SyntaxError`.** A name cannot be both a parameter (which is Local) and declared `global` in the same function; the two bindings contradict.
Nested `nonlocal` — inner function reaches which enclosing `x`? ::: The **nearest** enclosing function that has an `x`. Python walks outward through enclosing functions (E), skipping ones without `x`, and stops at the first that has it; it never falls through to Global.
Assigning to a global via `globals()['x'] = 5` from inside a function without `global` — does it work? ::: **Yes.** `globals()` returns the module namespace dict directly, and writing to that dict bypasses the local-creation rule entirely — no `global` keyword needed. (It also bypasses readability; prefer `global`.)
Reading a name that exists in Enclosing *and* Global — which wins? ::: **Enclosing (E)**, because LEGB searches E before G. The first match stops the search, so the outer function's variable shadows the module-level one.
A module-level (top-level) `x = x + 1` with no prior `x` — what happens? ::: **`NameError`.** At module level there is no separate "local" — the module scope *is* the global scope — so the read simply finds no `x` anywhere and raises `NameError`, not `UnboundLocalError`.
Can two comprehensions in the same function both use `x` without clashing? ::: **Yes.** Each comprehension has its own scope in Python 3, so their internal `x` bindings are independent and neither leaks into the function or into each other.
---
> [!recall]- One-line summary to lock it in
> Almost every trap here is really the same question: *are you reading a name, rebinding a name, or mutating an object?* Get that verb right and LEGB never surprises you.
---
## Connections
- [[1.2.30 Variable scope — LEGB rule (Local, Enclosing, Global, Built-in) (Hinglish)|Parent topic — LEGB]] — the rules these traps stress-test.
- [[NameError and UnboundLocalError]] — the two errors most traps produce.
- [[Closures and Nested Functions]] — the Enclosing-scope edge cases.
- [[Mutable vs Immutable arguments]] — the mutate-vs-rebind distinction.
- [[Namespaces and the dict model]] — why `globals()[...]` bypasses `global`.
- [[Modules and import]] — why `len` works but `sqrt` needs importing.