Every name in Python is resolved by searching through a hierarchy of scopes — the places where Python looks for the value of a variable. Python uses the LEGB rule: it searches Local scope first, then Enclosing (outer function) scope, then Global (module-level) scope, then Built-in scope. Understanding LEGB is essential for working with FastAPI’s dependency injection, closures, and module-level configuration objects. The most common scope-related bugs — accidentally shadowing a built-in name, misusing global variables, or being surprised by closure behaviour — all become obvious once you understand LEGB.
The LEGB Rule
# Python searches scopes in this order: Local → Enclosing → Global → Built-in
# ── Built-in scope — always available ─────────────────────────────────────────
# print, len, range, type, int, str, list, dict, set, None, True, False ...
print(len([1, 2, 3])) # len and print are built-in names
# ── Global scope — module level ───────────────────────────────────────────────
DATABASE_URL = "postgresql://localhost/mydb" # global variable
def connect():
print(DATABASE_URL) # reads the global — no global keyword needed for reading
connect() # postgresql://localhost/mydb
# ── Local scope — inside a function ───────────────────────────────────────────
def process():
result = 42 # local to process()
print(result)
process() # 42
# print(result) # NameError — result does not exist outside the function
# ── Enclosing scope — outer function's locals ─────────────────────────────────
def outer():
message = "Hello from outer" # enclosing scope for inner()
def inner():
print(message) # found in enclosing scope — no error
inner()
outer() # Hello from outer
global keyword, Python creates a new local variable with that name instead of modifying the global. This is a frequent source of confusion: the function appears to read the global but then creates a local copy when assigning, which hides the global for the rest of the function body.global variables for mutable application state (like a database connection pool or cached data). Instead, use FastAPI’s dependency injection system (Depends()) or application state (app.state). Module-level constants (configuration values that never change after startup) are fine as globals — the issue is with mutable globals that different parts of the code can modify in unexpected ways.list, type, id, input, print, or filter overwrites the built-in in that scope, and any code that tries to use the built-in function will find your variable instead. FastAPI developers most commonly shadow id (very common as a parameter name in route handlers) and filter. Use post_id, user_id, query_filter instead.The global Keyword
counter = 0 # global variable
def increment():
global counter # tell Python: 'counter' refers to the global
counter += 1
def read_counter():
print(counter) # reading global — no keyword needed
increment()
increment()
increment()
read_counter() # 3
# Without 'global', assignment creates a local:
def broken_increment():
counter += 1 # UnboundLocalError! Python sees the assignment and treats
# 'counter' as local, but it's used before being assigned locally
# ── The pattern that trips up JS developers ────────────────────────────────────
x = 10
def show():
print(x) # reads global x — fine
def modify():
x = 20 # creates LOCAL x — does NOT modify global
print(x) # prints 20 (local)
show() # 10
modify() # 20
show() # 10 — global unchanged!
The nonlocal Keyword
# nonlocal — modify a variable in the enclosing (outer function) scope
def make_counter(start=0):
count = start # enclosing scope variable
def increment():
nonlocal count # refers to enclosing 'count', not global
count += 1
return count
def reset():
nonlocal count
count = start
return increment, reset # return both functions (closure)
# Create a counter
inc, rst = make_counter(0)
print(inc()) # 1
print(inc()) # 2
print(inc()) # 3
rst()
print(inc()) # 1 — reset worked
# Without nonlocal, this would fail:
def broken():
count = 0
def inc():
count += 1 # UnboundLocalError — count is treated as local to inc()
Closures — Functions That Remember
# A closure is a function that captures variables from its enclosing scope
# The captured variables persist even after the outer function returns
def multiplier(factor):
def multiply(number):
return number * factor # 'factor' is captured from enclosing scope
return multiply
double = multiplier(2) # factor=2 is captured
triple = multiplier(3) # factor=3 is captured
print(double(5)) # 10
print(triple(5)) # 15
print(double(7)) # 14
# FastAPI uses closures for dependency factories:
def get_settings(env: str):
def load():
return Settings(environment=env) # env captured from outer scope
return load
# Each environment gets its own settings loader
prod_settings = get_settings("production")
dev_settings = get_settings("development")
LEGB Lookup — Step by Step
x = "global" # 3. Global scope
def outer():
x = "enclosing" # 2. Enclosing scope
def inner():
x = "local" # 1. Local scope
print(x) # "local" — found in L
def inner_no_local():
print(x) # "enclosing" — found in E (no local x)
inner() # local
inner_no_local() # enclosing
outer()
# Remove the global x — then inner_no_local would find it in E still.
# Remove both global and enclosing — would be NameError (not in B either)
Common Mistakes
Mistake 1 — Shadowing built-in names
❌ Wrong — overwriting the built-in list:
list = [1, 2, 3] # shadows the built-in list type
new = list([4, 5, 6]) # TypeError — list is now [1,2,3], not the constructor
✅ Correct — use descriptive names:
post_ids = [1, 2, 3] # ✓ descriptive, no shadowing
Mistake 2 — Expecting assignment inside a function to modify a global
❌ Wrong — assumes global is modified:
count = 0
def increment():
count = count + 1 # UnboundLocalError: count referenced before assignment
✅ Correct — declare global intent:
def increment():
global count
count = count + 1 # ✓
Mistake 3 — Loop variable capture in closures (classic bug)
❌ Wrong — all closures capture the same variable:
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] — all capture the final value of i
✅ Correct — capture by default argument:
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2] ✓
Quick Reference
| Scope | Where | Access |
|---|---|---|
| L — Local | Inside current function | Read/write freely |
| E — Enclosing | Outer function’s locals | Read freely; write with nonlocal |
| G — Global | Module level | Read freely; write with global |
| B — Built-in | Python builtins | Read freely; avoid shadowing |