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-006
Reported by
Seo, boarding announcements
What went missing
Three gate guide functions. A loop made one each for A1, B7 and C3, yet all three tell passengers to go to C3.
To close the case
Check what the three functions carry with them, and make each function remember its own gate.
Haneulseom International Airport · Baggage Center
Right after the morning announcements, three passengers who should be at three different gates are all standing in front of C3. Seo says three guide functions were created, so there should be three different announcements.
The supervisor holds up a carry-on bag. "Functions carry bags too. The question is whether what's inside is a value or a name tag." Today we open the little bag a function carries around: the closure.
As we saw in Lesson 1, functions are objects. You can bind them to names, put them in lists, pass them to other functions and return them.
def make_tag(owner):
def tag():
return "owner: " + owner
return tag
t = make_tag("kim")
print(t())
# owner: kim
print(t.__name__, t.__qualname__)
# tag make_tag.<locals>.tagmake_tag runs def tag(): and creates a new function object.t() is called, make_tag has already finished. By the rules of Lesson 5, the local name owner in make_tag should have disappeared with it.t() remembers "kim". How does the inner function hold on to a name of a function that has finished?__closure__ (9 min)When an inner function uses a name from the outer function, Python puts that name into a small bag called a cell. The outer function's box and the inner function point to the same cell. Even after the outer function ends and its box disappears, the inner function holds the cell, so the cell and the value inside it survive. A function that carries such a bundle of cells is a closure.
def make_tag(owner):
def tag():
return "owner: " + owner
return tag
t = make_tag("kim")
print(t.__code__.co_freevars)
# ('owner',)
print(type(t.__closure__[0]).__name__)
# cell
print(t.__closure__[0].cell_contents)
# kim
print(make_tag.__code__.co_cellvars)
# ('owner',)| Attribute | Meaning |
|---|---|
inner.__code__.co_freevars | Names the inner function borrows from outside (free variables) |
inner.__closure__ | A tuple of cells, one for each of those names |
cell.cell_contents | The object the cell currently points to |
outer.__code__.co_cellvars | Names the outer function has turned into cells |
On the tag board, while make_tag is running, owner in its box is shown with the value inside the cell. After the box disappears, check that the owner row of function tag still holds on to 'kim' through the cell.
Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
def make_tag(owner):
notes = ["memo"] * 1000
def tag():
return owner
return tag
print(make_tag("kim").__code__.co_freevars)
# ('owner',)Pin says
→ What a closure holds is not a copy of the value but a cell: the spot where a name tag hangs. So if the outside moves that name, the value the closure sees changes too.
Here is the reporter's code.
def make_guides(gates):
guides = []
for gate in gates:
guides.append(lambda: gate)
return guides
gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['C3', 'C3', 'C3']
print(gs[0].__closure__[0] is gs[2].__closure__[0])
# Truegate is one name inside make_guides, and there is one cell.lambdas carry that one cell. When the loop ends, the value in the cell is "C3".gate in the body of the lambda opens the cell when it is called. Nothing is written down when it is created. This is called late binding.Writing it as a list comprehension gives the same result.
fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# [2, 2, 2]
print(fs[0].__closure__[0] is fs[1].__closure__[0])
# TrueIt happens even without a loop. If you move the name outside after creating the function, the value the function sees changes too.
def make():
gate = "A1"
def guide():
return gate
gate = "Z9"
return guide
print(make()())
# Z9On the tag board below, watch the arrows from the three functions converge on one cell. In the fixed version, each function has its own defaults tuple.
Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
The fix is to store "the value at the moment of creation" in the function itself.
fs = [lambda i=i: i for i in range(3)]
print([f() for f in fs])
# [0, 1, 2]
from functools import partial
def guide(gate):
return "go to " + gate
guides = [partial(guide, gate) for gate in ["A1", "B7", "C3"]]
print([g() for g in guides])
# ['go to A1', 'go to B7', 'go to C3']lambda i=i: i: default arguments are evaluated once, at definition time (Lesson 4). The i of that moment goes into the function's __defaults__.functools.partial(guide, gate): creates a new callable that bundles the function with the argument of that moment. Its intent is clearer than the default-argument trick.f(5).nonlocal counter (7 min)Cells are not only for reading. Declared with nonlocal (Lesson 5), the inner function can move the tag inside the cell. Then the function has state that carries over from call to call.
def counter():
n = 0
def tick():
nonlocal n
n += 1
return n
return tick
c = counter()
c()
c()
print(c(), c.__closure__[0].cell_contents)
# 3 3
c2 = counter()
print(c2(), c())
# 1 4counter() creates a new cell. c and c2 know nothing about each other's numbers.nonlocal n, n += 1 makes n a local name and raises UnboundLocalError (the same cause as in Lesson 5).Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
You can do the same thing with a class. The state lives in the instance's attributes (Lesson 7).
class Counter:
def __init__(self):
self.n = 0
def tick(self):
self.n += 1
return self.n
k = Counter()
k.tick()
print(k.tick(), k.n)
# 2 2| Closure | Class | |
|---|---|---|
| Where the state lives | A cell | The instance's __dict__ |
| Seeing it from outside | Only via __closure__ | Directly, as k.n |
| Good fit | One action, a little state | Several actions, lots of state |
A closure that takes a function and returns a new function is called a decorator. The original function is stored in a cell of the inner function.
def log(func):
def wrapper(*args, **kwargs):
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def board(seat):
return "seat " + seat
print(board("12A"))
# call board
# seat 12A
print(board.__name__)
# wrapper@log is the same as board = log(board). The name board now points to the wrapper function, and the original board sits in wrapper's cell func.board.__name__ becomes wrapper. That is awkward for debugging and documentation.One line of functools.wraps copies the original function's name, docstring and so on onto the wrapper.
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def board(seat):
return "seat " + seat
print(board.__name__, board.__wrapped__.__name__)
# board boardDeeper topics, such as giving a decorator arguments or writing decorators as classes, are covered in the generators and decorators course.
All it takes is giving each function the gate from the moment it was created.
from functools import partial
def announce(gate):
return "go to " + gate
def make_guides(gates):
return [partial(announce, gate) for gate in gates]
gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['go to A1', 'go to B7', 'go to C3']
def make_guides_v2(gates):
return [lambda gate=gate: "go to " + gate for gate in gates]
print([g() for g in make_guides_v2(["A1", "B7", "C3"])])
# ['go to A1', 'go to B7', 'go to C3']From the next announcement on, each of the three passengers goes to their own gate. After Seo leaves, the supervisor leafs through the conveyor 13 records and mutters to herself. "Even things from a finished function don't get thrown away if someone keeps them in a carry-on bag. Maybe someone's holding on to those two bags too."
Closing report — LOST-006. The three guide functions shared one cell for the loop variable gate and read its last value, C3, when called (late binding). Solved by storing the value at creation time with partial and default arguments.
[lambda: i for i in range(3)] to [lambda: i * 10 for i in range(3)]? Use __closure__ to check how many cells there are.counter() so it returns two functions together, tick() and reset(). Check with is whether both use the same cell.count_calls that counts how many times a function has been called. Should the count live in an attribute of wrapper or in a cell? Try both.The supervisor's notebook
Functions are objects too. You can create a function and return it.
Outer names used by an inner function go into cells, and the function carries those cells in __closure__.
A closure holds a cell (a tag's spot), not a value. It opens it when called: late binding.
Functions created in a loop share one cell. Fix: lambda i=i: i, functools.partial.
Moving the tag in a cell with nonlocal gives a function with state. Lots of state calls for a class.
A decorator is a closure that takes a function and returns a function. Don't forget functools.wraps.
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.