Skip to content

Every button I made in a loop says the last name

  • Python · Core
  • 15 min read, about 1 h 5 min with practice
  • Concept
  • Asked often: medium
  • Reviewed 2026-11

You build a small app with one button per team member. Each button’s click function says hello to its person. You make them in a loop:

greeters = [lambda: f"Hi, {name}" for name in ["ana", "bo", "cy"]]

You click Ana’s button, then Bo’s:

>>> greeters[0]()
'Hi, cy'
>>> greeters[1]()
'Hi, cy'

Every button greets Cy. Each function was made while name was a different person. Why do they all say the last one?

  • Input: a list of names.
  • Output: a list of functions, one per name. Calling one, with no arguments, returns "Hi, " and its own name.
  • Rules: a function can’t be talked into greeting someone else: greeters[0]("Eve") is an error.
  • The hard part: a function made in a loop reads the loop variable when it’s called, and by then the loop has finished.

Example: [g() for g in make_greeters(["ana", "bo"])] gives ["Hi, ana", "Hi, bo"].

Hints open one at a time:

Hint 1 of 3

All three lambdas look up the same name variable, and the loop left it at "cy".

Hint 2 of 3

Each call of a function gets its own new variables.

Hint 3 of 3

Make each greeter inside its own call of a small function, make_greeter(name).

A closure is a function that uses a variable from the function it was made in. Python keeps that variable alive for it, even after the outer function has returned:

def make_greeter(name):
def greet():
return f"Hi, {name}" # name comes from make_greeter's call
return greet

It keeps the variable, not the value. When greet() runs, it looks up name at that moment. Each call of make_greeter makes a new name, so each greet has its own.

A loop is different: it doesn’t make a new variable each time round. The three lambdas share one name, and the loop leaves it at "cy".

Changing a kept variable needs nonlocal. Without it, count += 1 inside the inner function makes count local, and fails with UnboundLocalError.

Here are two greeters, each made in its own call. Each call gets its own box, and the box stays after the call ends, because greet still needs its name. The player stops to ask what happens next. Then type your own lines under it.

Each greeter keeps its own name

Built-inPython’s own names

  • print
  • len
  • max

Globalrunning now

no names yet

Step 1 of 15. Before the first line runs, the global box is empty. The built-in box holds names like print and len.

def make_greeter(name):
    def greet():
        return "Hi, " + name
    return greet
a = make_greeter("ana")
b = make_greeter("bo")
print(a(), b())

Try your own input

Indent with spaces. It knows def, return, global, nonlocal, pass, =, +=, -=, calls, whole numbers, text, True, False, None, lists with [i], .append() and .clear(), and the built-ins print, len, max, min, abs, sum and str.

All three make n functions in O(n). What changes is which name each one sees.

Okay

Lambdas in a loop

The idea: Make one lambda per name in a list comprehension.

Each lambda is a closure over name. Short, and it looks right.

from collections.abc import Callable
def make_greeters(names: list[str]) -> list[Callable[[], str]]:
return [lambda: f"Hi, {name}" for name in names] # noqa: B023 (the bug this rung shows)
Time
O(n)
Space
O(n)
Why
One function per name.

Where it breaks: all the lambdas share the comprehension’s one name. They run after the loop, so every one says Hi, cy.

Better

A default argument

The idea: Write lambda name=name: …, so each lambda stores the value when it’s made.

The waste in Okay: it looks the name up too late.

Defaults are worked out when the function is made, so each lambda stores its own name. Every button greets the right person.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_greeters(names: list[str]) -> list[Callable[..., str]]:
return [lambda name=name: f"Hi, {name}" for name in names]
Time
O(n)
Space
O(n)
Why
One function per name.
What changed from the previous rung
from collections.abc import Callable
def make_greeters(names: list[str]) -> list[Callable[[], str]]:
return [lambda: f"Hi, {name}" for name in names] # noqa: B023 (the bug this rung shows)
def make_greeters(names: list[str]) -> list[Callable[..., str]]:
return [lambda name=name: f"Hi, {name}" for name in names]

Where it breaks: the name is now a parameter, so greeters[0]("Eve") says Hi, Eve. A click handler that gets passed an event would greet the event. And readers often take name=name for a typo.

Best

A factory function

The idea: make_greeter(name) returns a greet that takes no arguments; call it once per name.

The waste in Better: it adds a parameter nobody should pass.

Each call of make_greeter has its own name, and its greet keeps it. greeters[0].__closure__[0].cell_contents is 'ana': that’s where Python keeps it.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_greeter(name: str) -> Callable[[], str]:
def greet() -> str:
return f"Hi, {name}" # name belongs to this call of make_greeter
return greet
def make_greeters(names: list[str]) -> list[Callable[[], str]]:
return [make_greeter(name) for name in names]
Time
O(n)
Space
O(n)
Why
One call, one function and one kept name per name.
What changed from the previous rung
from collections.abc import Callable
def make_greeters(names: list[str]) -> list[Callable[..., str]]:
return [lambda name=name: f"Hi, {name}" for name in names]
def make_greeter(name: str) -> Callable[[], str]:
def greet() -> str:
return f"Hi, {name}" # name belongs to this call of make_greeter
return greet
def make_greeters(names: list[str]) -> list[Callable[[], str]]:
return [make_greeter(name) for name in names]

When Best isn’t best: for a function with some arguments filled in, functools.partial(greet, "ana") says it in one call. Once a closure keeps several values or has several actions, a small class is easier to read.

Write make_greeters. The tests call each function you return.

One greeter per name

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: [g() for g in make_greeters(["ana", "bo", "cy"])] → ['Hi, ana', 'Hi, bo', 'Hi, cy']
  2. Not run yet: len(make_greeters([])) → 0
  3. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
WayEach sees its own nameCan be passed another nameUse it when
Okay: lambdas in a loopNo: all see the lastNoThe loop variable never changes, which is rare
Better: name=nameYesYes, by mistakeA quick script
Best: a factoryYesNoAny function made in a loop
  • The same thing happens with def in a loop, not only lambdas: every function reads the loop variable when called.
  • Changing a kept list needs no nonlocal: items.append(x) changes the list; it doesn’t point the name somewhere new.
  • Closures keep objects alive: a closure holding a big list keeps it in memory while the function exists.
  • global is not the fix: it makes every closure share one module variable.

Questions to ask first

  • Should each function be callable with no arguments?
  • Does the function need to change what it keeps, or only read it?

A 60-second walkthrough

“Closures capture variables, not values, and look them up when they’re called. The comprehension has one name variable, so every lambda sees its final value. The default-argument trick binds the value early, but adds a parameter callers can override. I’d use a small factory, make_greeter(name), that defines and returns greet. Each call has its own scope, so each greet closes over its own name. If the inner function needed to change a captured variable, I’d use nonlocal.”

Likely follow-ups

  • Where does Python keep the variable? In a cell. fn.__closure__ lists the cells, and fn.__code__.co_freevars their names.
  • Closure or class? A closure for one action with a little state; a class when there are several actions or the state needs a name.
  • How do decorators use this? A decorator returns a wrapper that closes over the original function. That’s the next lesson.

Three problems, from easy to hard. Each has hints, tests you can run here, and its own Okay, Better and Best answers. Try before you open them.

Write make_counter(). It returns a function tick() that returns 1, then 2, then 3. Two counters count on their own.

Hint 1 of 3

A global count is shared by every counter.

Hint 2 of 3

Keep count in make_counter, so each call gets its own.

Hint 3 of 3

To change count from tick, say nonlocal count.

Counters that don’t share

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: tick = make_counter(); [tick(), tick(), tick()] → [1, 2, 3]
  2. Not run yet: a and b = make_counter(); [a(), a(), b(), a()] → [1, 2, 1, 3]
Try your own input
Show the Okay, Better and Best answers
Okay

A global count

The idea: Keep count at module level and change it with global.

One counter works.

from collections.abc import Callable
count = 0
def make_counter() -> Callable[[], int]:
def tick() -> int:
global count
count += 1
return count
return tick
Time
O(1)
Space
O(1)
Why
One number.

Where it breaks: every counter adds to the same number, so a second counter starts at 3.

Better

A list in the closure

The idea: Keep count = [0] in make_counter and change count[0].

The waste in Okay: one number for every counter.

Changing what’s inside the list needs no nonlocal. This was the usual trick before Python 3.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_counter() -> Callable[[], int]:
count = [0] # a list, so tick can change what's inside it
def tick() -> int:
count[0] += 1
return count[0]
return tick
Time
O(1)
Space
O(1)
Why
One small list per counter.
What changed from the previous rung
from collections.abc import Callable
count = 0
def make_counter() -> Callable[[], int]:
count = [0] # a list, so tick can change what's inside it
def tick() -> int:
global count
count += 1
return count
count[0] += 1
return count[0]
return tick

Where it breaks: nothing breaks, but count[0] hides a plain number in a list, which readers have to work out.

Best

nonlocal

The idea: Keep count = 0 in make_counter and write nonlocal count in tick.

The waste in Better: a list used only to get round a missing keyword.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_counter() -> Callable[[], int]:
count = 0
def tick() -> int:
nonlocal count
count += 1
return count
return tick
Time
O(1)
Space
O(1)
Why
One kept number per counter.
What changed from the previous rung
from collections.abc import Callable
def make_counter() -> Callable[[], int]:
count = [0] # a list, so tick can change what's inside it
count = 0
def tick() -> int:
count[0] += 1
return count[0]
nonlocal count
count += 1
return count
return tick

When Best isn’t best: itertools.count(1) already counts: next(c) gives 1, 2, 3.

Write make_cached(func). It returns a function that gives the same results as func, but calls func only once for each different set of arguments, keyword ones included.

Hint 1 of 3

Keep a dict of results in make_cached; the closure keeps it alive.

Hint 2 of 3

args is a tuple, so it can be a dict key.

Hint 3 of 3

kwargs is a dict, which can’t be a key. tuple(sorted(kwargs.items())) can.

Remember the answers

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: f = make_cached(add); [f(1, 2), f(1, 2)] → [3, 3]
  2. Not run yet: f(1, 2); f(1, 2); f(5): how many real calls? → 2
  3. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
Show the Okay, Better and Best answers
Okay

Pass it through

The idea: A wrapper that always calls func.

The results are right.

from collections.abc import Callable
def make_cached(func: Callable) -> Callable:
def cached(*args: object) -> object:
return func(*args) # no cache yet: always calls func
return cached
Time
One call each time
Space
O(1)
Why
Nothing is kept.

Where it breaks: a slow func is called every time, even with the same arguments.

Better

A dict keyed by args

The idea: Keep results in a dict in the closure, keyed by the args tuple.

The waste in Okay: it works out the same answer again and again.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_cached(func: Callable) -> Callable:
results: dict = {}
def cached(*args: object) -> object:
if args not in results:
results[args] = func(*args)
return results[args]
return cached
Time
O(1) after the first call
Space
O(k)
Why
One entry for each of the k different calls.
What changed from the previous rung
from collections.abc import Callable
def make_cached(func: Callable) -> Callable:
results: dict = {}
def cached(*args: object) -> object:
return func(*args) # no cache yet: always calls func
if args not in results:
results[args] = func(*args)
return results[args]
return cached

Where it breaks: f(1, b=2) raises TypeError: the wrapper takes no keyword arguments.

Best

Keyword arguments in the key

The idea: Add tuple(sorted(kwargs.items())) to the key, and pass **kwargs on.

The waste in Better: it can’t take keyword arguments at all.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_cached(func: Callable) -> Callable:
results: dict = {}
def cached(*args: object, **kwargs: object) -> object:
key = (args, tuple(sorted(kwargs.items())))
if key not in results:
results[key] = func(*args, **kwargs)
return results[key]
return cached
Time
O(1) after the first call
Space
O(k)
Why
One entry per different call.
What changed from the previous rung
from collections.abc import Callable
def make_cached(func: Callable) -> Callable:
results: dict = {}
def cached(*args: object) -> object:
if args not in results:
results[args] = func(*args)
return results[args]
def cached(*args: object, **kwargs: object) -> object:
key = (args, tuple(sorted(kwargs.items())))
if key not in results:
results[key] = func(*args, **kwargs)
return results[key]
return cached

When Best isn’t best: use functools.cache or lru_cache, which do this in C and can limit the size. And f(1, 2) and f(1, b=2) are kept as two entries here, though they are the same call.

Write make_editor(). It returns a dict of three functions that share one text: "type" adds text to the end, "undo" removes the last typed piece, and "show" returns the text. Two editors are separate. Typing thousands of times must not keep thousands of copies of the text.

Hint 1 of 3

Module-level text is shared by every editor.

Hint 2 of 3

Saving the whole text before each type works, but keeps a full copy per keystroke.

Hint 3 of 3

Keep the typed pieces in a list: undo pops the last one, and show joins them.

An editor with undo

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: type "Hello", type ", world"; show → 'Hello, world'
  2. Not run yet: type, type, undo; show → 'Hello'
  3. Not run yet: a and b = make_editor(); a types "hi"; b shows → ''
  4. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
Show the Okay, Better and Best answers
Okay

Global text

The idea: Keep the text and a history at module level.

One editor works.

from collections.abc import Callable
text = ""
history: list[str] = []
def make_editor() -> dict[str, Callable]:
def type_text(more: str) -> None:
global text
history.append(text)
text += more
def undo() -> None:
global text
if history:
text = history.pop()
def show() -> str:
return text
return {"type": type_text, "undo": undo, "show": show}
Time
O(n) per type
Space
O(n²)
Why
Each type copies the text so far; history keeps every copy.

Where it breaks: a second editor shows the first one’s text.

Better

Text in the closure

The idea: Keep text and history in make_editor, and change text with nonlocal.

The waste in Okay: every editor shares one text.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_editor() -> dict[str, Callable]:
text = ""
history: list[str] = []
def type_text(more: str) -> None:
nonlocal text
history.append(text) # a full copy of the text, every time
text += more
def undo() -> None:
nonlocal text
if history:
text = history.pop()
def show() -> str:
return text
return {"type": type_text, "undo": undo, "show": show}
Time
O(n) per type
Space
O(n²)
Why
history keeps a full copy of the text for every keystroke.
What changed from the previous rung
from collections.abc import Callable
text = ""
history: list[str] = []
def make_editor() -> dict[str, Callable]:
text = ""
history: list[str] = []
def type_text(more: str) -> None:
global text
history.append(text)
nonlocal text
history.append(text) # a full copy of the text, every time
text += more
def undo() -> None:
global text
nonlocal text
if history:
text = history.pop()
def show() -> str:
return text
return {"type": type_text, "undo": undo, "show": show}

Where it breaks: 3,000 pieces of 10 letters keep 3,000 copies of a growing text, about 45 million letters in all.

Best

Keep the pieces

The idea: Keep a list of typed pieces: type appends, undo pops, show joins.

The waste in Better: it saves the whole text to undo one piece.

No nonlocal is needed: pieces always points to the same list.

Highlighted lines are new in this rung.

from collections.abc import Callable
def make_editor() -> dict[str, Callable]:
pieces: list[str] = [] # each typed piece, in order
def type_text(more: str) -> None:
pieces.append(more)
def undo() -> None:
if pieces:
pieces.pop()
def show() -> str:
return "".join(pieces)
return {"type": type_text, "undo": undo, "show": show}
Time
O(1) per type, O(n) per showwas O(n) per type
Space
O(n)
Why
Each letter is stored once.
What changed from the previous rung
from collections.abc import Callable
def make_editor() -> dict[str, Callable]:
text = ""
history: list[str] = []
pieces: list[str] = [] # each typed piece, in order
def type_text(more: str) -> None:
nonlocal text
history.append(text) # a full copy of the text, every time
text += more
pieces.append(more)
def undo() -> None:
nonlocal text
if history:
text = history.pop()
if pieces:
pieces.pop()
def show() -> str:
return text
return "".join(pieces)
return {"type": type_text, "undo": undo, "show": show}

When Best isn’t best: with redo, cursor moves and deletes, a class with named methods is clearer than a dict of functions.

A few quick questions. Ones you miss are remembered, so you can find your weak spots later.

  1. Predict the outputWhat does this print?
    fns = [lambda: i for i in range(3)]
    print([f() for f in fns])
  2. Spot the bugCalling counter() raises UnboundLocalError. Why?
    def make_counter():
        count = 0
        def counter():
            count += 1
            return count
        return counter
  3. Multiple choiceWhich fix makes each lambda in a loop keep its own name?
  4. Predict the outputWhat does this print?
    def make_adder(n):
        def add(x):
            return x + n
        return add
    
    a, b = make_adder(1), make_adder(10)
    print(a(5), b(5))

Add these to your review deck. Each card comes back a little later every time you get it right.

  • A closure is an inner function that uses a variable of the outer one; Python keeps that variable alive.
  • It reads the variable when called, so functions made in a loop all see the last value.
  • Fix: one call of a factory function per value.
  • nonlocal x to point a kept name at a new value; changing what’s inside a kept list needs nothing.
  • fn.__closure__ holds the kept cells.

Can you do these now?

If you’re not sure of any of these, scroll back to that part before moving on.

Status: Not started