PY-01-012-P · Primer — Scope & Namespaces
The idea in one sentence
A scope is the region of your code where a name (like x or total) points to a particular value, and Python has clear rules for which value wins when the same name appears in more than one place.
Why it exists
When you write x = 5 inside a function and there is also an x = 99 outside it, Python needs an unambiguous rule to decide which x you mean. Without scopes, every variable in every function would collide with every other, and large programs would be impossible to reason about. Scopes let each function keep its own private set of names — a namespace — so you can reuse a friendly name like count or result in a hundred functions without any of them stepping on each other. They also protect you: a function can read the outside world by default, but can't accidentally overwrite it unless you explicitly ask.
How it works
Python resolves a name by searching four scopes in a fixed order — the LEGB rule: Local (inside the current function), Enclosing (any function wrapping this one), Global (the top level of the file/module), and Built-in (print, len, and friends). The first match wins, and the search stops there. Assignment is the twist: writing to a name (x = ...) makes it local by default, which is why the global keyword lets a function rebind a module-level name, and nonlocal lets an inner function rebind a name from its enclosing function. A closure happens when an inner function "remembers" a variable from its enclosing scope even after the outer function has returned. In the example below, each returned click carries its own live count:
message = "global" # global scope
def make_counter():
count = 0 # enclosing scope
def click():
nonlocal count # rebind the enclosing 'count'
count += 1
return count
return click
def announce():
global message
message = "changed by announce()"
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(message) # global
announce()
print(message) # changed by announce()
# Output:
# 1
# 2
# global
# changed by announce()
The one gotcha
Reading a global variable inside a function works with no ceremony — but the moment a function assigns to that same name anywhere in its body, Python treats the name as local for the entire function, even on lines before the assignment. So total += 1 on a global total (without global total) raises UnboundLocalError, because += is really "read then assign," and the read now looks for a local that hasn't been created yet. The fix is to declare global total (or nonlocal for an enclosing name) so Python knows you mean the outer one.
See it — LEGB name resolution, innermost first
flowchart TD
A["Look up a name<br/>e.g. count"] --> B{"Local scope?<br/>this function"}
B -->|found| Z["Use this value"]
B -->|no| C{"Enclosing scope?<br/>outer function"}
C -->|found| Z
C -->|no| D{"Global scope?<br/>top of module"}
D -->|found| Z
D -->|no| E{"Built-in scope?<br/>print, len, ..."}
E -->|found| Z
E -->|no| F["NameError:<br/>name is not defined"]
The one-line takeaway
Names are resolved Local → Enclosing → Global → Built-in, and any assignment makes a name local unless you say global or nonlocal.
Where you'll meet it
You'll hit scope every time you write a helper function, build a counter or accumulator, use a decorator or a factory that returns a function, or debug a surprise UnboundLocalError. It's also the foundation for understanding closures, callbacks, and why mutating a global from deep inside your code is usually a smell worth refactoring.
Named concepts: local vs global scope, the LEGB resolution rule, global and nonlocal keywords, closures over enclosing variables
