DMarlon Studio

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.

Python · Closures and nonlocal in DMarlon Studio: the example's code and its output in the terminal

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.
  • nonlocal lets 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.