A realistic case: a per-user rate limiter
Each limiter remembers its own call counts. The counts live in a dict, and changing a dict's contents is not rebinding, so no nonlocal is needed. The two limiters never share state.
A closure is a function that remembers the variables of the function it was created in, even after that outer function has returned. Closures make counters, function factories and private state possible, and they have one famous trap: late binding in loops.
Change the code and press Run (Ctrl+Enter). Predict what will happen first, then break it on purpose and read what changes. Your edits are saved and match the lesson page.
Ready for more? This phase has 6 hands-on exercises with automatic checks.
Practice questions
Write the code in the editor, run it, then open the model answer to compare.
Write make_greeter(greeting) that returns a function taking name and returning e.g. Hi, Asha!. Create two greeters with different greetings.
Write make_accumulator() that returns a function. Each call add(n) adds n to a running total and returns it. Call it with 10, 5 and 1.
Create a list of three functions with a loop so that calling them returns 10, 20 and 30, avoiding the late-binding trap.
Explain it without notes
What is a closure and what problem does it solve?
Why does [lambda: i for i in range(3)] give three functions that all return 2?
When do you need nonlocal?
How do two closures made from separate calls of the same factory relate?
Expected output
api asha True
api asha True
api ravi True
api asha False
login asha True False
remembered: {'asha': 3, 'ravi': 1}
free variables: ('calls', 'max_calls')