Every button I made in a loop says the last name
See the problem
Section titled “See the problem”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?
Define the task
Section titled “Define the task”- 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"].
Pause and think
Section titled “Pause and think”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).
Understand the idea
Section titled “Understand the idea”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 greetIt 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.
Built-inPython’s own names
- 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( ))
Climb the ladder
Section titled “Climb the ladder”All three make n functions in O(n). What changes is which name each one sees.
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.
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.
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 it yourself
Section titled “Write it yourself”Write make_greeters. The tests call each function you return.
One greeter per name
Ctrl + Enter runs- Not run yet:
[g() for g in make_greeters(["ana", "bo", "cy"])]→['Hi, ana', 'Hi, bo', 'Hi, cy'] - Not run yet:
len(make_greeters([]))→0 - Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Compare
Section titled “Compare”| Way | Each sees its own name | Can be passed another name | Use it when |
|---|---|---|---|
| Okay: lambdas in a loop | No: all see the last | No | The loop variable never changes, which is rare |
Better: name=name | Yes | Yes, by mistake | A quick script |
| Best: a factory | Yes | No | Any function made in a loop |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- The same thing happens with
defin 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.
globalis not the fix: it makes every closure share one module variable.
Say it in the interview
Section titled “Say it in the interview”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
namevariable, 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 returnsgreet. Each call has its own scope, so eachgreetcloses over its ownname. If the inner function needed to change a captured variable, I’d usenonlocal.”
Likely follow-ups
- Where does Python keep the variable? In a cell.
fn.__closure__lists the cells, andfn.__code__.co_freevarstheir 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.
Practice
Section titled “Practice”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.
1. Counters that don’t share (easy)
Section titled “1. Counters that don’t share (easy)”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- Not run yet:
tick = make_counter(); [tick(), tick(), tick()]→[1, 2, 3] - 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
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.
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 tickWhere it breaks: nothing breaks, but count[0] hides a plain number in a list, which readers have to work out.
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 tickWhen Best isn’t best: itertools.count(1) already counts: next(c) gives 1, 2, 3.
2. Remember the answers (medium)
Section titled “2. Remember the answers (medium)”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- Not run yet:
f = make_cached(add); [f(1, 2), f(1, 2)]→[3, 3] - Not run yet:
f(1, 2); f(1, 2); f(5): how many real calls?→2 - 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
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.
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 cachedWhere it breaks: f(1, b=2) raises TypeError: the wrapper takes no keyword arguments.
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 cachedWhen 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.
3. An editor with undo (hard)
Section titled “3. An editor with undo (hard)”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- Not run yet:
type "Hello", type ", world"; show→'Hello, world' - Not run yet:
type, type, undo; show→'Hello' - Not run yet:
a and b = make_editor(); a types "hi"; b shows→'' - 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
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.
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.
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.
Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”- 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 xto point a kept name at a new value; changing what’s inside a kept list needs nothing.fn.__closure__holds the kept cells.
Go deeper
Section titled “Go deeper”- Why do lambdas defined in a loop all return the same result?, in the Python FAQ.
- The
nonlocalstatement. - Before this: Scope and the LEGB rule. Next: Decorators.
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