Two loops that look the same. Watch what each list of functions answers after the loop has run.
functions = []
for n in [1, 2, 3]:
functions.append(lambda: n)
print([f() for f in functions]) # -> [3, 3, 3]
# The name n still exists, and it still holds 3
print(n) # -> 3
# Bind the value into its own scope, one per function
def make(n):
return lambda: n
functions = [make(n) for n in [1, 2, 3]]
print([f() for f in functions]) # -> [1, 2, 3]
- The lambda holds the name n, not the number
Nothing is looked up until the function is called. By then the loop has finished and n holds its final value, so all three functions read the same 3. The functions are not identical — they are three separate objects — but they all consult the same variable, which is why they all agree.
- A loop does not make a new scope; a function call does
Python has function scope, not block scope, so every iteration reuses one n that outlives the loop. Calling make(n) creates a fresh local n for each call, and each returned lambda closes over its own. That is the whole fix: put a function call between the loop and the closure. A comprehension does not count — it has its own scope, but still only one loop variable inside it.
- The default-argument trick works for a different reason
lambda price, percent=percent: ... is the other common fix, and it works because default arguments are evaluated once, at definition time, freezing the current value. It is shorter, and it puts an extra parameter into the public signature that any caller can override by accident. Know it, because you will read it; prefer the factory when the function is handed to somebody else.
Add print(percent) inside the lambda and call one of the returned functions. The value it prints is not the one that was there when the lambda was created — ask yourself when Python actually looks that name up.
All three lambdas share one variable, because a for loop does not create a new scope for each iteration — the name outlives the loop. You need each function to have its own copy of the percentage.
Write a small factory that takes a percentage and returns a function, then call it once per iteration. The call creates a new local variable each time, and each returned function closes over its own. functools.partial does the same job in one line if you shape the arithmetic as a two-argument function.
def make_discounts(percentages): def discount_of(____): def apply(price): return price * (100 - ____) / 100 return ____ return [____(percent) for percent in percentages]
def make_discounts(percentages):
def discount_of(percent):
def apply(price):
return price * (100 - percent) / 100
return apply
return [discount_of(percent) for percent in percentages]
Every correct answer to this bug does the same thing underneath: put a function call between the loop and the closure, so that each closure gets its own scope to look in. discount_of(percent) is that call. Once you see the fix as 'give each one its own scope' rather than as a trick, you can invent it again in a language whose rules you have not memorised.
Write the arithmetic once as def discounted(percent, price), then return [partial(discounted, percent) for percent in percentages]. partial stores the argument at the moment it is called, so there is no shared variable left to change. It is the neatest version when the underlying operation deserves a name of its own — and it keeps the real function testable directly, without going through the factory.
lambda price, i=i: price * (100 - percentages[i]) / 100 fixes the symptom and keeps a live reference to the caller’s list. The functions then quietly change meaning when someone edits that list — a bug that surfaces days later, in a different file, with no obvious connection to this one. Capture the value you need, not a route back to where it lives.
If the functions are built once from a constant list and never handed anywhere, this whole shape may be more than the job needs — a dict of percentage to price, or a single discounted(price, percent) that callers invoke directly, is often simpler than manufacturing functions at all. Build functions in a loop when something downstream genuinely wants callables it can store and invoke later: handlers, hooks, a toolbar. When you do, remember that the same late-binding rule applies to every one of them.
- Rewrite it with functools.partial and check that every case still passes, including the mutation one.
- Give each returned function a useful __name__ or __doc__ mentioning its percentage, so a traceback tells you which discount blew up.
- Try the same bug in a comprehension — [lambda price: price * (100 - percent) / 100 for percent in percentages] — and confirm for yourself that the comprehension’s own scope does not save you.