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 6

Time 4:10–5:00

Carry-on bags: closures

Lost-baggage report

LOST-006

Three passengers, all sent to the same gate

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.

6-1. Functions that return functions (7 min)

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>.tag
  • Every call to make_tag runs def tag(): and creates a new function object.
  • By the time 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.
  • Yet t() remembers "kim". How does the inner function hold on to a name of a function that has finished?

6-2. The carry-on bag: cells and __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',)
AttributeMeaning
inner.__code__.co_freevarsNames the inner function borrows from outside (free variables)
inner.__closure__A tuple of cells, one for each of those names
cell.cell_contentsThe object the cell currently points to
outer.__code__.co_cellvarsNames 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)

When make_tag finishes, its box disappears, but the cell held by the function tag keeps hold of 'kim'.
  • The carry-on bag contains only the names the inner function actually uses. The outer function's other local names are cleaned up when it ends.
  • Conversely, whatever goes in stays as long as that function lives. If the inner function uses a big list even once, that list stays along with it.
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.

6-3. Late binding: why all three passengers went to C3 (10 min)

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])
# True
  • Loops do not create zones (Lesson 5). Through all three rounds, gate is one name inside make_guides, and there is one cell.
  • All three lambdas carry that one cell. When the loop ends, the value in the cell is "C3".
  • The 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])
# True

It 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()())
# Z9

On 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)

With lambda: gate, the gate of all three functions points to the same cell. Fixed with gate=gate, each function stores the value from its creation time in a default argument.

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.
  • The default-argument approach has a downside: callers can overwrite the value, as in f(5).

6-4. Functions with state: a 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 4
  • Every call to counter() creates a new cell. c and c2 know nothing about each other's numbers.
  • Without 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)

Each call to tick changes the value in the cell from 0 to 1 to 2. Integers are immutable, so the integer does not change; the tag in the cell moves to a new integer.

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
ClosureClass
Where the state livesA cellThe instance's __dict__
Seeing it from outsideOnly via __closure__Directly, as k.n
Good fitOne action, a little stateSeveral actions, lots of state

6-5. A taste of decorators (7 min)

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.
  • That is why 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 board

Deeper topics, such as giving a decorator arguments or writing decorators as classes, are covered in the generators and decorators course.

Case closed (10 min)

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.

Exercises

  1. What happens if you change [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.
  2. Change counter() so it returns two functions together, tick() and reset(). Check with is whether both use the same cell.
  3. Write a decorator 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.

← Previous

Lesson 5 · Zones and passes: scope

Next →

Lesson 7 · The bag spec: classes and instances

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