Reading is easy, assigning is the trap
A function can read an outer name without ceremony:
x = "global"
def show():
print(x) # fine
But assigning to that name inside the function does not change the outer one. It creates a new local name that happens to be spelled the same, and it disappears when the function returns.
The rule that catches everyone
If a name is assigned anywhere in a function, it is local for the entire function — including lines that run before the assignment.
def broken():
print(x) # UnboundLocalError
x = "too late"
That print looks like it should read the global, and it would have, if the line below it did not exist. Python decides local-or-not when it compiles the function, not while running it. The error message — "local variable referenced before assignment" — is precise once you know this, and baffling before.
global and nonlocal
To rebind rather than shadow, say so:
def bump():
global count
count += 1
global reaches module level. nonlocal reaches the nearest enclosing function, which is what makes closures able to keep state:
def counter():
n = 0
def step():
nonlocal n
n += 1
Both are worth knowing and neither is worth reaching for often. A function that rebinds globals is a function whose behaviour depends on when you call it, which is exactly the kind of thing that makes bugs hard to find. Returning a value is nearly always better.
Mutating is not assigning
One clarification that resolves a lot of confusion: items.append(1) is not an assignment. It mutates the object the name already points at, so it affects the outer list without needing global. items = [1] is an assignment, and creates a local. The distinction is the object versus the name.
Why the rule exists
Deciding local-or-not at compile time looks like an odd choice until you consider the alternative. If Python worked out scope while running, then whether a name was local could depend on which branch had executed, and the same line could mean two different things on two different runs. Instead the compiler scans the whole function body once: if a name is assigned anywhere in it, that name is local everywhere in it.
That single rule explains the UnboundLocalError that catches everyone. The read on line one is a read of a local variable, because line five assigns to it, and the local has no value yet. The error message is precise; it just describes a decision made before the function ran.
Closures, and why they are useful
A function defined inside another function can see the enclosing function's variables, and it keeps seeing them after the outer function has returned. That combination - an inner function plus the variables it captured - is a closure, and it is how a function can carry state without a class or a global.
The counter on this page is the small version. The pattern scales to configuration: a function that builds and returns another function with some settings already baked in. make_formatter(currency="GBP") returns something you can call many times without repeating the currency.
nonlocal exists because assigning inside the inner function would otherwise create a new local there, exactly as it would at module level. It says "this name belongs to the function that wraps me", and it is the only way to update captured state rather than merely read it.
Globals, and when they stop being harmless
Reading a module-level constant from inside a function is ordinary and fine - that is what constants are for. Writing to a module-level name from inside a function is where trouble starts, because the function's behaviour now depends on what ran before it, and testing it in isolation stops being possible.
The practical test is whether calling the function twice with the same arguments gives the same answer. If a global statement means it does not, the state probably wants to be a parameter, a return value, or an attribute on an object - all three of which make the dependency visible in the signature rather than buried in the body.
Shadowing builtins
list, dict, set, id, type, sum, input and max are all ordinary names that happen to be defined in the builtin scope, and assigning to any of them inside your code hides the original for the rest of that scope. It is legal and it produces confusing failures later, usually when something calls list(x) and gets a TypeError about your list not being callable.
Watching the four scopes resolve
LEGB is easier to trust once you have seen it choose:
x = "global"
def outer():
x = "enclosing"
def inner():
print("inner sees:", x)
inner()
outer()
print("module sees:", x)
inner sees: enclosing
module sees: global
inner never assigns x, so it looks outward: not local, so enclosing — and it stops there, at outer's x, without ever reaching the module-level one. The module's x is untouched, because outer's assignment created a name in outer rather than changing the global.
Now the version that fails:
def broken():
print(x)
x = "too late"
broken()
UnboundLocalError: cannot access local variable 'x' where it is not associated
with a value
There is a perfectly good global x, and the print does not reach it. The assignment on the line *below* made x local for the whole function, so the print is reading a local that has not been given a value yet.
The wording of that error changed in Python 3.11; older versions say "local variable 'x' referenced before assignment". Both describe the same thing, and the newer one is clearer about it.
Comprehensions have their own scope, loops do not
Two things that look similar and differ, which explains a family of small surprises.
A for loop does not create a scope. The loop variable is an ordinary local, it survives after the loop ends, and it overwrites anything of the same name. This is why for i in range(3) leaves i set to 2 afterwards, and why reusing a name in nested loops silently shadows.
A comprehension does create one. Its loop variable lives only inside it, does not leak, and does not clobber a name of the same spelling outside. This changed in Python 3 specifically because the leaking was a common source of bugs.
The consequence worth remembering is that a comprehension can read enclosing names but cannot see a class body's names. A comprehension written directly in a class body that refers to another class attribute raises NameError, because the class body is not a function scope and the comprehension's scope skips straight past it to the module. It is the one genuinely strange corner of Python's scoping, and the fix is to compute the value outside the class or pass it in through the iterable.
Where scope shows up in practice
The rules are short; the situations where they bite are worth naming.
A function that "does not update" a counter. Assigning to a module-level name inside a function creates a local instead. The function appears to run and the value never changes. global fixes it and is usually the wrong fix — returning the new value and assigning at the call site keeps the dependency visible.
A callback that captured the wrong value. Functions built in a loop capture the *variable*, not its value, so they all see the final one. Binding it as a default argument — lambda i=i: ... — captures at definition time. This is the same late-binding behaviour that makes closures useful, seen from the wrong side.
A shadowed builtin. Naming a variable list, dict, sum, id or input hides the builtin for the rest of that scope. The failure comes later, usually as TypeError: 'list' object is not callable, at a line that looks correct.
A name that only exists sometimes. Assigning inside an if and reading after it works when the branch ran and raises UnboundLocalError when it did not. Initialise before the branch.
All four are the same rule seen from different angles: assignment decides where a name lives, and it decides it for the whole function.
Why there is no block scope
Coming from C, Java or JavaScript, the most surprising rule is that an if or a for does not create a scope. A name assigned inside one is visible after it, and the loop variable outlives the loop.
This is deliberate, and the reasoning is the same as everywhere else in the language: functions are the unit of encapsulation, and adding a second, finer kind of scope would mean two sets of rules for readers to hold. A function body is one namespace, and where in that body a name was first assigned does not change what it refers to.
It has practical consequences worth using rather than fighting. A value computed inside an if is available afterwards, so the common pattern of declaring a variable before a branch purely so it exists later is unnecessary — assign it in both branches instead. A loop can leave its last value behind on purpose, which is occasionally the neat way to say "the last item that matched".
It also has one real hazard: a name assigned only inside a branch that did not run does not exist, and reading it raises UnboundLocalError. The compiler knows the name is local, so it does not fall back to a global of the same spelling. Initialising before the branch, or assigning on every path, is the fix — and the error is at least loud rather than silently reading something from an outer scope.
The one exception, comprehensions, exists precisely because the leaking there was a genuine problem rather than a convenience.
Questions people ask
Does an if or a for block create a scope? No. Only functions, classes, modules and comprehensions do.
Can I read a global without declaring it? Yes. global is only needed to *assign* to one.
What is the difference between global and nonlocal? global reaches module level; nonlocal reaches the nearest enclosing function. nonlocal fails if there is no such name.
Why does items.append(1) work without global? Because it mutates the object rather than rebinding the name. Only assignment is affected.
Does a class body count as an enclosing scope? No, and this is the exception that surprises people — methods do not see class-level names without self or the class name.
Can I list what is in scope? locals() and globals() return dictionaries of the current names, which is occasionally useful for debugging.
Is shadowing a builtin ever fine? In a two-line function where it is obvious, it does no harm. As a habit it costs more than the shorter name saves.
Does del x remove a name from scope? It unbinds it, so a later read raises NameError or UnboundLocalError. It does not reach into an outer scope.
Why can a method not see class attributes directly? Because a class body is not an enclosing scope for its methods. Reach them through self. or the class name.
What scope does a lambda have? The same as any function: its own local scope, with the enclosing one visible. That is why the loop-capture trap applies to lambdas exactly as it does to def.
Recap in one screen
- Names resolve local, enclosing, global, builtin — first match wins.
- If a name is assigned anywhere in a function it is local everywhere in that function, including lines above the assignment.
global and nonlocal rebind rather than shadow, and both are usually a sign that a return value would be better.- Mutating is not assigning:
items.append(x) needs no declaration, items = [x] creates a local. - A
for loop shares the enclosing scope; a comprehension has its own.
Scope says who can see a name, not who can see it at the same time
Everything above assumes one thread of execution. Add a second and every rule still holds — a call still gets its own local namespace, global still rebinds at module level — but a question appears that scope cannot answer. Two threads running the same function have separate locals, and *share* every global, along with every mutable object those locals happen to point at.
That sharing is where the hard bugs live. counter += 1 on a module-level integer is a read, an add and a write, and the other thread can be scheduled in either gap.