Name Tag Airport: Python's Object Model
·
Course overview
1
A tag is not the bag: names and objects
2
Sealed bags: mutable and immutable
3
Bags inside bags: containers and copying
4
Handing over a bag: functions and arguments
5
Zones and passes: scope
6
Carry-on bags: closures
7
The bag spec: classes and instances
8
Standard airport procedures: dunder methods
9
The spec behind the spec: inheritance and MRO
10
The checkpoint: how attribute access really works
11
Unclaimed bags: memory and GC
12
Locker numbers: hashing, equality and the big picture
Lost-baggage report
LOST-005
Reported by
Han, security screening operations team
What went missing
A counter for the number of bags screened. scanned = 0 is clearly right at the top, yet scan() raises UnboundLocalError as soon as it is called.
To close the case
Explain why the function could not see the outside name, and fix the counter so it counts properly.
Haneulseom International Airport · Baggage Center
Supervisor Do Miran taps the airport map on the wall. "Gate, terminal, airport, country. Passes are checked from the inside out. If it's found on the inside, nobody even looks outside."
Today's report is about the security screening counter. They put scanned = 0 outside and tried to bump it by one inside a function, but the very first line fails with "there is no such value". The supervisor points to the last line of the function. "Here's the culprit. The line that assigns a value."
When reading a name, Python searches four zones in order, from the inside out. It stops at the first place it finds the name.
| Order | Zone | What it is | At the airport |
|---|---|---|---|
| L | Local | Inside the function currently running | Gate |
| E | Enclosing | The outer functions wrapped around that function | Terminal |
| G | Global | The module where the function is defined | Airport |
| B | Built-in | Basic names like len, print, list | Country |
zone = "G"
def terminal():
gate = "E"
def boarding():
seat = "L"
print(seat, gate, zone, len("abc"))
boarding()
terminal()
# L E G 3seat is found inside boarding (L), and gate in the enclosing function terminal (E).zone is found in the module (G), and len in the built-ins (B).NameError.boarding from somewhere else, gate is still looked up in terminal.The scope map below does not run the code; it only reads it and shows which zone each name is found in. The broken function at the end is the star of the next section.
Scope map
1
zone = "G"
2
3
def terminal():
4
gate = "E"
5
def boarding():
6
seat = "L"
7
print(seat, gate, zone, len("abc"))
8
boarding()
9
10
def broken():
11
print(zone)
12
zone = "L"
Name
seat @ terminal. seat is a local name of the inner function boarding, so it is not visible from the outer terminal. The search always goes from the inside out.zone @ broken is marked "Found here" at L. That means the name was decided to be a local name of that function, not that it holds a value.Here is the reporter's code.
scanned = 0
def scan(bag):
print(bag, scanned)
scanned = scanned + 1
scan("T1")
# UnboundLocalError: cannot access local variable 'scanned' where it is not associated with a valueThe error comes from the print on line 4, not from line 5. Line 5 has not even run yet. Even so, Python does not look for scanned on line 4 outside the function.
There is one rule. If a name is assigned anywhere inside a function, that name is local throughout the whole function. This decision is made when the function is compiled, not when it runs.
def scan(bag):
print(bag, scanned)
scanned = scanned + 1
print(scan.__code__.co_varnames)
# ('bag', 'scanned')
print(issubclass(UnboundLocalError, NameError))
# Trueco_varnames is the list of local names the compiler has fixed. scanned is already in it.scanned", and that tag does not hang on any bag yet. That is exactly what the error message says.UnboundLocalError is a kind of NameError.Lines that never run count just the same.
x = 10
def f():
print(x)
if False:
x = 1
f()
# UnboundLocalError: cannot access local variable 'x' where it is not associated with a value"Assigning" is not only =. +=, for x in ..., import x, def x():, class x:, with ... as x, except ... as x and del x all make a local name in that function too.
Compare the three versions on the tag board below. The first is the reporter's code, and its last step shows an error.
Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
Pin says
→ When you see UnboundLocalError, look below the failing line. Somewhere in the same function, that name is being assigned.
global and nonlocal (7 min)If you want to assign to a name in an outer zone, declare it first.
scanned = 0
def scan(bag):
global scanned
scanned = scanned + 1
scan("T1")
scan("T2")
print(scanned)
# 2def terminal():
boarded = 0
def board():
nonlocal boarded
boarded += 1
board()
board()
return boarded
print(terminal())
# 2global x: in this function, x is the module's name. Assigning to it moves the module's tag.nonlocal x: in this function, x is the name in the nearest enclosing function. It cannot be used for module names.SyntaxError: no binding for nonlocal 'x' found before anything runs.global x later gives SyntaxError: name 'x' is assigned to before global declaration.When you only read a name, no declaration is needed. boarding in 5-1 read gate and zone without any declaration. Declarations are needed only when you assign.
global quietly changes state outside the function, which makes the function hard to test on its own. It is usually better to take the value as an argument and return the new value. nonlocal comes back with closures in Lesson 6.
In Python, new zones are created by functions (def, lambda), comprehensions and class bodies. if, for, while, with and try blocks do not create zones.
for i in range(3):
pass
print(i)
# 2
if True:
inside = "if"
print(inside)
# if
sq = [n * n for n in range(3)]
print(sq)
# [0, 1, 4]
print(n)
# NameError: name 'n' is not defined| Creates a zone | Does not create a zone |
|---|---|
def, lambda, list/set/dict comprehensions, generator expressions, class bodies | if, for, while, with, try |
for ends, the loop variable i stays with its last value (2). The same goes for names created inside the loop.n of a comprehension does not leak out. Since 3.12, CPython no longer builds a separate function for a comprehension and runs it inline instead (PEP 709), but the rule that names don't leak still holds.Built-in names live in the outermost zone (B). So if you create a global with the same name, the global is found first from then on.
list = ["T1", "T2"]
print(list("ab"))
# TypeError: 'list' object is not callableThe tag list now hangs on a list bag in the global zone, so list("ab") tries to call that list and fails. The built-in list has not disappeared. It is only hidden.
list = ["T1", "T2"]
del list
print(list("ab"))
# ['a', 'b']
import builtins
print(builtins.list is list, builtins.len("abc"))
# True 3list, and the built-in list is visible again.builtins module. Even when they are hidden, you can always get them back with builtins.list.del print after shout = print gives NameError: name 'print' is not defined. There is no global tag named print, so there is nothing to remove.Don't use names like list, dict, str, id, type, input, max or sum as variables. They hide the built-in functions throughout that file.
locals(), globals(), vars(), dir() (4 min)globals(): the dictionary of the module zone. Called at module level, it is the real storage itself.locals(): the names of the current zone. At module level it is the same as globals().vars(obj): the object's __dict__. Called without arguments, it is the same as locals() (Lesson 7).dir(): returns just a sorted list of names.Since 3.13, the meaning of locals() inside a function is clearly defined (PEP 667). Every call returns a snapshot of that moment as a new dictionary. Changing that dictionary does not change the local variables.
def check():
y = 1
locals()["y"] = 99
print(y, locals()["y"])
snap = locals()
y = 2
print(snap["y"], locals()["y"])
check()
# 1 1
# 1 2
def where(a):
b = 2
return dir(), vars()
print(where(1))
# (['a', 'b'], {'a': 1, 'b': 2})locals()["y"] = 99 changes only the snapshot that was just made. The variable y is still 1.snap keeps the value from the moment it was taken (1). Only a fresh call to locals() shows 2.locals().There are two ways to fix the counter: move the outside tag directly (global), or take the value and return the new value.
scanned = 0
def scan(bag):
global scanned
print(bag, scanned)
scanned = scanned + 1
scan("T1")
scan("T2")
print(scanned)
# T1 0
# T2 1
# 2
def scan_count(bag, count):
print(bag, count)
return count + 1
total = 0
total = scan_count("T1", total)
total = scan_count("T2", total)
print(total)
# T1 0
# T2 1
# 2Han picks the second approach. Even when the number of screening lanes grows, each lane can keep its own count. The supervisor adds, "Every zone has its own tags. If you want to move a tag in someone else's zone, show your pass first."
Closing report — LOST-005. Because of scanned = ... at the bottom of the function, scanned became a local name for the whole function, causing UnboundLocalError on the first line. Solved by taking the value as an argument and returning it.
zone = "L" below the print inside boarding from 5-1? Compare it with zone @ broken on the scope map.nonlocal, build a counter that counts by calling append on a list in the outer function. Why does it work without a declaration? (Hint: moving a tag vs. changing the bag.)sum = 0 at the top of a module, then call sum([1, 2, 3]). What error do you get? Fix it using builtins.The supervisor's notebook
Names are looked up in the order L → E → G → B, stopping at the first match.
Zones are decided by where a function is written, not where it is called.
Assign a name anywhere in a function and it is local throughout the function. Hence UnboundLocalError.
global and nonlocal only when moving an outside tag. Reading needs no declaration.
Loops and if do not create zones; functions and comprehensions do.
list = … hides the built-in list. Built-in names live in builtins.
locals() inside a function is a snapshot (3.13, PEP 667). Changing it leaves the variables alone.
← Previous
Lesson 4 · Handing over a bag: functions and arguments
Next →
Lesson 6 · Carry-on bags: closures
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.