PythonMastery

Functions made in a loop all see the last value

A lambda or inner function looks up loop variables when it's called, not when it's made, so every one sees the final value. Bind it with a default.

A function made inside a loop doesn't remember the loop variable's value at that moment. It remembers the variable, and looks it up when it's called. By then the loop has finished.

The surprise

python
rates = {"student": 0.10, "staff": 0.20, "vip": 0.30}

discounts = {}
for group, rate in rates.items():
    discounts[group] = lambda price: round(price * (1 - rate), 2)

print(discounts["student"](100), discounts["vip"](100))
output
70.0 70.0

The student price should be 90.0. Every lambda uses the last rate the loop saw: 0.30.

The fix

Bind the current value as a default argument. Defaults are evaluated when the function is made, which is exactly the moment you want:

python
rates = {"student": 0.10, "staff": 0.20, "vip": 0.30}

discounts = {}
for group, rate in rates.items():
    discounts[group] = lambda price, rate=rate: round(price * (1 - rate), 2)

print(discounts["student"](100), discounts["vip"](100))
output
90.0 70.0

Or use functools.partial, which stores the argument explicitly and reads more clearly when the function isn't a one-liner:

python
from functools import partial

def apply_discount(rate, price):
    return round(price * (1 - rate), 2)

rates = {"student": 0.10, "staff": 0.20, "vip": 0.30}
discounts = {group: partial(apply_discount, rate) for group, rate in rates.items()}
print(discounts["student"](100), discounts["staff"](100))
output
90.0 80.0

Why it works

A lambda, like any nested function, is a closure: it keeps a reference to the variables of the scope it was made in, not copies of their values. The loop reuses one rate variable, so every closure points at the same one, holding whatever it held last. List comprehensions behave the same way: [lambda x: x * n for n in range(3)] gives three functions that all multiply by 2.

When not to use it

The default-argument trick has a cost: rate becomes a parameter anyone can override by accident (discounts["vip"](100, 0)). When that matters, prefer partial or a small class. And if you only call the functions inside the same loop iteration, there's no problem to fix.

Learn it properly: Lambda & Functional Programming, Functions & Scoping