Learning Goals
3 minBy the end of this lesson you can:
- Say where a name created inside a function can and cannot be seen.
- Explain why a function can read a global but not change one.
- Read
NameErrorandUnboundLocalErrorand know which is which. - Replace a
globalwith a parameter and areturn.
Warm-Up · Which Line Crashes?
5 minThree prints, and exactly one of them fails. Decide which, and why, before running it.
shop = "Shop Mia" def open_shop(): greeting = "Welcome" print(greeting) # A print(shop) # B open_shop() print(greeting) # C
Answer
C fails with NameError: name 'greeting' is not defined.
A works: greeting was created inside the function and is being read inside it. B works: a function can see names created outside it. C fails because greeting was born inside open_shop and died the moment the function ended.
New Concept · Two Places a Name Can Live
12 minLocal. Any name first assigned inside a function — including its parameters — belongs to that function. It is created when the call starts and thrown away when the call ends. Call the function twice and you get a fresh set both times.
Global. A name assigned at the left margin of your file belongs to the whole module. Functions can read it.
GST = 0.06 # global def with_tax(amount): tax = amount * GST # reads the global, creates a local return amount + tax print(with_tax(100)) # 106.0 print(tax) # NameError - tax lived inside with_tax
Every function in this section used a name like total or line. If those all shared one space, function number seven would quietly overwrite function number three's working. Locals are what let you write a function without knowing — or caring — what every other function in the file called its variables.
Reading is allowed; changing is not. This surprises everyone once:
count = 0
def add_one():
count = count + 1 # UnboundLocalError!
add_one()Python decides before running whether each name is local or global. Because count is assigned somewhere inside add_one, it is treated as local throughout the function — including on the right-hand side, where it has not been given a value yet. Hence UnboundLocalError: cannot access local variable 'count' where it is not associated with a value.
The two error messages are worth telling apart:
NameError— this name does not exist here at all. Usually a typo, or a local escaping its function.UnboundLocalError— this name is local (because you assign to it somewhere below), but you read it before assigning.
The global keyword exists, and you should mostly not use it.
count = 0
def add_one():
global count # works... but
count = count + 1It works. It also means this function can no longer be understood on its own, cannot be reused anywhere else, and can be broken by any other function in the file. The alternative is one line longer and has none of those problems:
def add_one(count): """Return count plus one.""" return count + 1 score = 0 score = add_one(score) score = add_one(score) print(score) # 2
Data in through the brackets, answers out through return. That is the whole rule, and it is why lessons 2 and 3 came before this one.
Worked Example · The Same Program, Twice
12 minA canteen till, written both ways. Type both and run them — they print the same thing.
version A · globals
total = 0
items = 0
def add(price):
global total, items
total = total + price
items = items + 1
def show():
print(f"{items} items, $ {total:.2f}")
add(4.50)
add(2.00)
show()version B · parameters and returns
def add(total, items, price): """Return the new total and item count after adding one price.""" return total + price, items + 1 def summary(total, items): """Return the till summary as a string.""" return f"{items} items, $ {total:.2f}" total, items = 0, 0 total, items = add(total, items, 4.50) total, items = add(total, items, 2.00) print(summary(total, items))
Version A is shorter. Version B is the one you want, and here is the test that shows why.
Now open a second till. In version B, that is one more pair of names:
till2_total, till2_items = 0, 0 till2_total, till2_items = add(till2_total, till2_items, 12.00) print(summary(till2_total, till2_items))
In version A it is impossible without rewriting add, because add does not take a till — it takes the till, the only one, by name.
"Could I copy this function into a different program and have it work?" Version B's add yes; version A's no, because it silently depends on two names that happen to exist next to it. That is the practical cost of a global, and it arrives long before the program is large.
Try It Yourself
13 minRun each snippet, write down the exact error, and say in one sentence which rule it broke.
# a
def show():
print(message)
show()
# b
def show():
message = "hi"
show()
print(message)
# c
lives = 3
def lose():
lives = lives - 1
lose()Answer
- a —
NameError. Nothing calledmessageexists anywhere. - b —
NameErroron the last line.messagewas local toshowand is gone. - c —
UnboundLocalError. Assigning tolivesmakes it local for the whole function, so reading it first fails.
Rewrite this so hp is never global. damage should take the current hp and return the new one, and the game loop should keep the value.
hp = 100
def damage(amount):
global hp
hp = hp - amount
if hp < 0:
hp = 0
damage(30)
damage(45)
print(hp)Hint
def damage(hp, amount): """Return hp after taking amount of damage, never below zero.""" return max(hp - amount, 0) hp = 100 hp = damage(hp, 30) hp = damage(hp, 45) print(hp)
Now try what version A could not do: give a second character their own hp and hurt them with the same function.
Mini-Challenge 🔥 · The Function That Ignores You
8 minNur's quiz program never adds up. No error, no crash — the score is always 0. She has read the code five times.
score = 0
def mark_answer(given, correct):
score = 0
if given == correct:
score = score + 1
return score
mark_answer("b", "b")
mark_answer("a", "a")
mark_answer("c", "a")
print("Score:", score)Two questions: which score does line 4 create, and what happened to the three values the function returned?
Answer
Line 4 creates a local score that shadows the global one. It starts at 0 on every call, goes up to 1 for a correct answer, and is returned — and then thrown away, because none of the three calls stores the result. The global score was never touched by anything, so it is still 0.
Two separate mistakes, and the fix uses lesson 3 more than this one:
def is_correct(given, correct): """Return 1 for a correct answer, 0 otherwise.""" if given == correct: return 1 return 0 score = 0 score += is_correct("b", "b") score += is_correct("a", "a") score += is_correct("c", "a") print("Score:", score) # 2
The lesson underneath: a function that returns a value you ignore is code that does nothing. If a call sits on a line by itself, either it prints, or it changes something, or it is pointless.
Recap
3 minNames assigned inside a function are local: created on the call, gone at the end, invisible outside. Functions can read globals but not assign to them — try, and Python treats the name as local everywhere in that function, giving UnboundLocalError. global lifts the restriction and costs you a function that can no longer stand alone. Pass data in as parameters and hand answers back with return, and the problem disappears.
Vocabulary Card
- scope
- The region of a program where a name can be seen.
- local
- A name belonging to one call of one function. Includes its parameters.
- global
- A name assigned at the left margin of the file, readable everywhere in it.
- shadowing
- A local name that hides a global of the same name — the cause of "it never updates" bugs.
- UnboundLocalError
- Reading a name that Python decided is local because you assign to it later in the same function.
Homework
4 minWrite a small game or till program that keeps at least two changing values — score and lives, or total and items — using no global anywhere. Every function takes what it needs and returns what it changed.
Then prove the design: add a second player or a second till, without editing a single one of your functions. Write one sentence saying which lines you had to add.
Hint: a function that changes two things can return both — return score, lives — and the caller unpacks them with score, lives = ....
Sample · dua_pemain.py
# dua_pemain.py - no globals anywhere; every function is portable import random def roll(): """Return one dice roll.""" return random.randint(1, 6) def take_turn(score, lives): """Play one turn. Return the new score and lives.""" value = roll() if value == 1: return score, lives - 1 return score + value, lives def report(name, score, lives): """Return a one-line status for a player.""" return f"{name:<8} score {score:>3} lives {lives}" # player one Mia_score, Mia_lives = 0, 3 # player two - three added lines, and not one function changed wei_jie_score, wei_jie_lives = 0, 3 for turn in range(5): Mia_score, Mia_lives = take_turn(Mia_score, Mia_lives) wei_jie_score, wei_jie_lives = take_turn(wei_jie_score, wei_jie_lives) print(report("Mia", Mia_score, Mia_lives)) print(report("Leo", wei_jie_score, wei_jie_lives))
Three lines added for the second player, zero functions touched. If adding a player meant editing take_turn, something inside it was reaching out to a name it should have been given.