Lesson 4 · 4 of 20
Closures and nonlocal · Python
Functions that return functions and remember their variables: Python closures, and nonlocal to change them, with discounts and tickets.

In Python a function can define another one inside it and return it. make_discount(0.20) creates the apply function and returns it without calling it. The interesting part is that apply can still see rate after make_discount has finished: that is a closure, a function that carries the variables of the place where it was created.
Each call makes a different closure: sales remembers 0.20 and members remembers 0.35. They work like small factories of pre-configured functions.
Reading an outer variable needs nothing special, but changing it does. In next_ticket, number += 1 would make Python treat number as local (and fail with UnboundLocalError). The `nonlocal number` statement says it is the outer function's variable, the one that survives between calls.
That is why bakery and butcher keep separate counts: each dispenser has its own number. The last line peeks inside: sales.__closure__[0].cell_contents is the cell where the closure stores rate.
Closures are the foundation of decorators (the next lesson) and a lightweight alternative to a class when you only need to keep one piece of data and one method.
Code · main.py
def make_discount(rate): """Returns a function that remembers rate.""" def apply(price): return round(price * (1 - rate), 2) return applydef make_ticket_dispenser(prefix): """Each call hands out the next ticket number.""" number = 0 def next_ticket(): nonlocal number # change the outer variable number += 1 return f"{prefix}-{number:03d}" return next_ticketsales = make_discount(0.20)members = make_discount(0.35)print("Sales price:", sales(50.0))print("Members price:", members(50.0))bakery = make_ticket_dispenser("B")butcher = make_ticket_dispenser("M")print(bakery(), bakery(), bakery())print(butcher(), butcher())print("Next bakery ticket:", bakery())# The remembered values live in the closure cellsprint("Captured rate:", sales.__closure__[0].cell_contents)Output · python3 main.py
Sales price: 40.0 Members price: 32.5 B-001 B-002 B-003 M-001 M-002 Next bakery ticket: B-004 Captured rate: 0.2
Key points
- An inner function can use the outer function's variables.
- Those variables stay alive after the outer function returns.
nonlocallets you change a variable of the enclosing function.- Each call to the factory creates an independent closure.
Exercise
Write make_counter(limit) that returns a function counting 1, 2, 3… and returning "full" once it goes past limit.