R

ReflexKR

Reflex Docs

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

Name Tag Airport: Python's Object Model
Lesson 5

Time 3:20–4:10

Zones and passes: scope

Lost-baggage report

LOST-005

A name right next door that can't be found

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."

5-1. The order names are looked up in: LEGB (9 min)

When reading a name, Python searches four zones in order, from the inside out. It stops at the first place it finds the name.

OrderZoneWhat it isAt the airport
LLocalInside the function currently runningGate
EEnclosingThe outer functions wrapped around that functionTerminal
GGlobalThe module where the function is definedAirport
BBuilt-inBasic names like len, print, listCountry
zone = "G"

def terminal():
    gate = "E"
    def boarding():
        seat = "L"
        print(seat, gate, zone, len("abc"))
    boarding()

terminal()
# L E G 3
  • seat 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).
  • If a name is in none of the four zones, you get a NameError.
  • Zones are decided by where a function is written, not where it is called from. Even if you call 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

The four names inside boarding are found in L, E, G and B respectively. zone inside broken stops at L, even though it exists outside.
  • Pick 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.

5-2. Where you assign decides the scope (10 min)

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 value

The 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))
# True
  • co_varnames is the list of local names the compiler has fixed. scanned is already in it.
  • So line 4 looks for "the local tag 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)

In the first version the scan box holds only the bag tag, and the scanned tag is empty. global uses the outside tag, and the third version takes the value as an argument and returns it.

Pin says

→ When you see UnboundLocalError, look below the failing line. Somewhere in the same function, that name is being assigned.

5-3. Moving an outside tag directly: 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)
# 2
def terminal():
    boarded = 0
    def board():
        nonlocal boarded
        boarded += 1
    board()
    board()
    return boarded

print(terminal())
# 2
  • global 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.
  • If the enclosing functions have no such name, you get SyntaxError: no binding for nonlocal 'x' found before anything runs.
  • The declaration must come before the name is used. Assigning first and writing 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.

5-4. What creates a zone and what doesn't (5 min)

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 zoneDoes not create a zone
def, lambda, list/set/dict comprehensions, generator expressions, class bodiesif, for, while, with, try
  • After the for ends, the loop variable i stays with its last value (2). The same goes for names created inside the loop.
  • The 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.

5-5. Shadowing built-in names (5 min)

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 callable

The 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 3
  • Remove the global tag list, and the built-in list is visible again.
  • Built-in names live in the builtins module. Even when they are hidden, you can always get them back with builtins.list.
  • Here is the answer to Lesson 1, exercise 3. Running 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.

5-6. Looking into zones: 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.
  • If you want to create names dynamically, use a dictionary, not locals().

Case closed (10 min)

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
# 2

Han 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.

Exercises

  1. What happens if you add a line zone = "L" below the print inside boarding from 5-1? Compare it with zone @ broken on the scope map.
  2. Without using 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.)
  3. Put 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.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex