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.