A variable's scope is the region of the program where a name can be seen and used. A name you create inside a function is local - it exists only while that function runs and is invisible everywhere else. A name created at the top level, outside any function, is global, and every function can read it.
counter = 0 # global
def show():
message = "hi" # local to show()
print(message, counter)
show() # hi 0
print(message) # NameError: message is not defined
message lives only inside show. counter is global, so show reaches it freely.
flowchart LR G["module scope<br/>counter = 0"] --> F["function scope<br/>message = 'hi'"] F -. "exists only while<br/>the function runs" .-> G style G fill:#3776ab,color:#fff style F fill:#6b7280,color:#fff
Why scopes are kept separate
Scope is what stops your functions from treading on each other. Imagine every function shared one big pool of names: the i in your loop, the total in a running sum, and the temp in someone else's swap would all collide, and a stray assignment in one function would silently rewrite a variable three functions away. Debugging that would be hopeless.
Local scope gives each call a private workspace. You can name a variable n inside show without checking whether some other function already used n, because each one gets its own. Globals still exist for values the whole module shares, but the default - private, disposable locals - is what keeps programs predictable.
Lifetime: locals do not outlive the call
A local variable is born when its function starts and destroyed when it returns. Nothing about it persists into the next call, so the function does not remember what happened last time - each call begins with a clean slate.
def add_one_local():
total = 0
total += 1
return total
print(add_one_local()) # 1
print(add_one_local()) # 1 again, not 2
total is recreated at 0 on every call, so the function always returns 1. If a value genuinely needs to survive between calls, a local is the wrong tool - you store it at module level, or return it and let the caller hold on to it.
flowchart LR C["call the function"] --> N["fresh local scope<br/>locals are created"] N --> R["function returns"] R --> G["scope discarded<br/>locals are gone"] style N fill:#3776ab,color:#fff style G fill:#6b7280,color:#fff
Reading a global is allowed; assigning is not
There is an asymmetry beginners trip over constantly. A function may read a global name it never assigns to - that lookup quietly walks outward to the module scope. But the moment you assign to that same name inside the function, Python assumes you mean a brand-new local, and builds one that shadows the global for the duration of the call.
total = 0 # global
def reset_locally():
total = 99 # a LOCAL total, not the global
print(total) # 99
reset_locally()
print(total) # still 0 - the global was untouched
The module-level total is never modified, because the assignment created a separate local that happened to share its name. Reading reaches outward; writing stays inside, unless you say otherwise.
counter = 0 # global
def reset_locally():
counter = 99 # a LOCAL variable, not the global
print("inside:", counter)
reset_locally()
print("outside:", counter)
total = 0 # global
def add_one():
global total # without this line, total += 1 raises UnboundLocalError
total += 1
add_one()
add_one()
print("total:", total)
flowchart TD G["module: counter = 0"] R["function READS counter<br/>sees the module value, 0"] L["function ASSIGNS counter = 99<br/>a NEW local, not the global"] G --> R L -. "the module counter<br/>stays 0" .-> G style G fill:#3776ab,color:#fff style L fill:#b45309,color:#fff style R fill:#1e293b,color:#fff
The global keyword
When a function truly must change a module-level variable, you say so out loud with the global keyword. Placing global total at the top of the body tells Python: this name refers to the module-level total, so assignments reach the real one instead of making a local.
total = 0 # global
def add_one():
global total
total += 1
add_one()
add_one()
print(total) # 2 - now it remembers
The declaration is deliberate - it forces you to admit, in writing, that the function edits shared state. Anything that sneaks around that requirement, as the next section shows, tends to blow up rather than work by accident.
The sane use: configuration constants
Not every global is a hazard. Values the whole program treats as fixed - a tax rate, a page size, a retry limit - belong at module level in UPPER_CASE, where every function can read them without the global keyword. Reading is always permitted, so TAX_RATE is visible inside any function that needs it, and one edit at the top updates every use at once.
This is the normal, healthy relationship with module-level names: many readers, no writers. The sharp edges are reserved for functions that assign to a shared name, which the next section unpacks. That is why the add_tax exercise reads its rate from the module rather than receiving it as an argument.
The trap: UnboundLocalError
Mixing a read and a write on the same global name inside one function produces a genuinely confusing error. Suppose count is global and you write:
count = 0 # global
def increment():
count = count + 1 # UnboundLocalError!
Because count appears on the left of an assignment, Python decides count is local for the entire function - not just that line. That decision retroactively turns the count on the right side into a local too, and since the local has no value yet, you get UnboundLocalError before anything runs. The cure is the keyword from above: declare global count, and both sides refer to the module variable. You will repair exactly this bug in the exercises.
The global TAX_RATE is set to 0.08. Write add_tax(amount) that reads the global (do not declare it global - you are only reading) and returns amount plus the tax on it.
TAX_RATE = 0.08
def add_tax(amount):
# read the global TAX_RATE and return amount + its tax
pass
Reading a global needs no keyword - just use the name.
Tax on amount is amount * TAX_RATE; return amount plus that.
TAX_RATE = 0.08
def add_tax(amount):
return amount + amount * TAX_RATE
The global score starts at 0. Write total_score(new_points) that adds new_points to the global score and returns the new total. This time you are writing, so you need the global keyword.
score = 0
def total_score(new_points):
# add new_points to the global score, then return it
pass
Declare the name with
global scorebefore you assign to it.Then
score = score + new_pointsandreturn score.
score = 0
def total_score(new_points):
global score
score = score + new_points
return score
What does this print? The function has its own local n. Predict both lines, then Check.
def bump():
n = 10
n = n + 1
return n
n = 100
print(bump())
print(n)
Inside bump(), n starts at 10 and becomes 11, which is what it returns.
The global n is a different variable; the function never touches it, so it stays 100.
This function tries to increase the global count and return it, but it crashes with UnboundLocalError. Inside a function, assigning to a name makes Python treat it as local for the whole body - so even the read on the right-hand side looks at a local that has no value yet. Declare it global to fix it.
count = 0
def increment():
count = count + 1
return count
The name is assigned inside the function, so Python calls it local - declare it otherwise.
Add
global countas the first line of the body.
count = 0
def increment():
global count
count = count + 1
return count
Two functions never share their local variables, even when one calls the other. Write inner() that returns its own local message set to 'world', and outer() that sets its own local message to 'hello' and returns the two messages joined with a space as 'hello world'. Include both definitions.
def inner():
# your own local message
pass
def outer():
# your own local message, then use inner()
pass
Each function has its own local
message- the same name, two separate boxes.outer() returns its own message plus a space plus whatever inner() returns.
def inner():
message = 'world'
return message
def outer():
message = 'hello'
return message + ' ' + inner()
Recap
- A name's scope is where it is visible: local inside a function, global at module level.
- A local's lifetime is one call - it is rebuilt each time and discarded on return, so functions do not remember previous calls.
- A function can read a global freely, but assigning to that name creates a local that shadows it.
- Use
globalonly when a function must write shared module state - and preferreturn. - Reading and assigning the same global name makes Python treat it as local everywhere, raising
UnboundLocalError.
Next you will meet Python's containers - sets and tuples - where scope decides what a function can reach.