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
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))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:
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))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:
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))
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.