Scope: local, global, and why your variable is not changing
You change a variable inside a function, print it outside, and it has not
changed. Or you get a NameError for a variable that is clearly right there.
Both come from scope — the rules about where a name is visible — and ten minutes
here saves a long, confusing afternoon.
Local names
A name created inside a function exists only inside it:
def calculate():
result = 42
print(result)
calculate()
print(result)
42
NameError: name 'result' is not defined
result was created when the function ran and destroyed when it finished. From
outside, it never existed.
This is a feature, not a limitation. It means you can use total or i inside
a function without wondering whether some other part of the program uses the
same name. Each function gets its own private space.
Global names
A name defined at the top level of a file is visible everywhere, including inside functions:
TAX_RATE = 0.18
def apply_tax(amount):
return amount + (amount * TAX_RATE)
print(apply_tax(100))
118.0
The function reads TAX_RATE without being passed it. Convenient, and the main
legitimate use is exactly this: constants.
Reading is allowed, assigning creates a local
Here is the rule that causes the confusion:
count = 0
def increment():
count = count + 1
increment()
UnboundLocalError: cannot access local variable 'count' where it is not
associated with a value
That error looks absurd. count is right there, with a value.
What happened: Python scans a function before running it. Seeing count = ...
anywhere inside, it decides count is a local name for the whole function.
Then the right-hand side runs first and tries to read local count, which has
no value yet. Hence the error.
Assigning to a name anywhere in a function makes it local everywhere in that function, including on lines above the assignment.
Without the assignment it reads the global fine:
count = 0
def show():
print(count) # works
show()
So this works and that fails, and the only difference is whether you assign.
The global keyword, and why to avoid it
You can override the rule:
count = 0
def increment():
global count
count = count + 1
increment()
print(count) # 1
It works. You should still almost never use it.
A function that modifies a global can change the behaviour of code far away from itself. Debugging means finding every function that touches that name. Testing becomes awkward because tests affect each other. And you cannot tell from a call site that anything outside was modified.
Pass values in and return values out instead:
def increment(count):
return count + 1
count = 0
count = increment(count)
Slightly longer, and completely predictable. Everything the function needs arrives through its parameters; everything it produces leaves through its return value.
Use global for essentially nothing. Constants read-only at the top of a
file are fine. Mutable global state is a habit worth never forming.
The exception you will actually hit
global is about rebinding a name. Modifying an object is different:
items = []
def add(value):
items.append(value) # no error, no global needed
add("apple")
print(items) # ['apple']
No assignment to items here — you are reading it and calling a method on it.
The name still points at the same list; the list's contents changed.
So a function can modify a global list or dictionary without global, which is
precisely why mutable global state is hard to reason about. Nothing in the
function signature warns you.
Parameters are local too
def double(n):
n = n * 2
return n
value = 5
print(double(value)) # 10
print(value) # 5
n is a local name. Rebinding it does nothing to the caller's value. This is
the same point as the arguments lesson, from the other direction: immutable
values are safe, and mutable ones are not.
The lookup order
When Python meets a name it looks in four places, in order:
- Local — inside the current function
- Enclosing — inside any function wrapped around it
- Global — the top level of the file
- Built-in —
print,len,sumand the rest
First match wins. Which is why this is a bad idea:
list = [1, 2, 3]
print(list("abc"))
TypeError: 'list' object is not callable
Your global list shadowed the built-in, so the function is gone for the rest
of the program. The names worth avoiding: list, dict, set, str, int,
sum, max, min, type, id, input, file.
Your editor usually colours built-ins differently. If a variable name is
coloured like print, choose another.
Nested functions
A function inside a function can read the outer one's names:
def outer():
message = "hello"
def inner():
print(message) # reads the enclosing scope
inner()
outer()
You will not write these often yet. They matter because it is how closures work, and closures are why the decorators in later frameworks behave as they do. For now, knowing the enclosing scope exists is enough.
Practice
- Create a variable inside a function and try to print it outside. Read the
NameError. - Define
TAX_RATEat the top level and read it inside a function. Confirm it works without passing it. - Reproduce the
UnboundLocalErrorexactly. Then explain, in one sentence, why removing the assignment makes the error disappear. - Fix that function twice: once with
global, once by passing and returning. Say which you would rather see in a codebase and why. - Write a function that appends to a global list without
global. Confirm it works, then explain why it does when the counter version did not. - Write
double(n)that reassignsn, and show the caller's variable is unchanged. - Create a variable called
listand then try to uselist(). Read the error. Restart your REPL to recover. - Take a function of yours that reads a global and rewrite it to take that value as a parameter. Notice it is now testable with any value you like.
Next: functions that accept any number of arguments.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship