Build Stateful Closures with nonlocal
Return a closure that remembers state and use nonlocal only when rebinding that state.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Build Stateful Closures with nonlocal
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A Python function can return another function, and the returned function can still use values from the outer call. That sounds abstract until you want a small counter, a callback with a remembered setting, or a validator configured once and used many times. Today we will build two counters that remember independent totals. The first version will fail with UnboundLocalError on its very first increment, even though the outer function clearly assigned count. We will inspect why Python treats the name as local in the inner function, then add one declaration, nonlocal, to update the enclosing binding. We will verify that two separate calls to the factory do not accidentally share
- 00:42
state. Along the way, we will distinguish assigning to a captured name from mutating an object stored under that name. That distinction explains why a captured list may work without nonlocal while an integer counter does not. The companion PythonLessonLab page has runnable examples and a checked task. You can pause after the broken run, predict what the first call does, and then compare your explanation with the actual exception. The outer call created count with value zero and returned advance. The function did not lose all connection to the outer scope merely because broken_counter returned.
- 01:22
The problem is more specific: inside advance, the augmented assignment counts as an assignment to count. Python decides that count is a local name in advance. To calculate the new value, it first tries to read that local name, but no local value has been assigned yet. That produces UnboundLocalError. A common wrong repair is to put count at module level and declare it global. That makes this one counter seem to work, but then every counter instance would affect the same shared value. Our intention is to give each factory call private state. Another wrong repair is to initialize a new local count inside advance every time;
- 02:03
then the result always starts over. Instead, we need to tell Python that the inner assignment should target the count in the enclosing function. The exception name points us toward a scope issue rather than an arithmetic issue. The greeting reads prefix from its enclosing call. No assignment to prefix happens inside greet, so Python can use the captured binding without any declaration. The collector looks up items and calls append on the list object. The list changes, but the name items is not assigned to a different object. That is why the code does not need nonlocal. If we wrote `items = items + [value]`, the assignment would make items local in add
- 02:48
and reproduce the same kind of binding problem as the integer counter. This distinction matters when you read unfamiliar closure code: ask whether the inner function merely reads a name, mutates the object under it, or rebinds the name itself. Integer addition necessarily produces a new integer, so `count += step` rebinds count. The returned functions still remember their outer environments after those outer calls finish. Closures are not a special global store; a new factory call builds a new environment. This gives us a precise repair: declare the enclosing integer binding as nonlocal before assigning to it.
- 03:29
The nonlocal statement belongs in the inner function, where the assignment occurs. It refers to the count binding in the nearest enclosing function scope. It cannot invent a new outer variable that never existed. The first counter starts at zero and reaches one, then three after a step of two. The second counter starts at ten and reaches eleven. Calling first with a zero step leaves it at three, and calling second again reaches twelve. Those alternating results show that the counters are independent. We did not use a module global, and we did not recreate count on every call. This design is appropriate for a small piece of private state with one simple
- 04:10
operation. If the state gains many fields, validations, or methods, a class may be easier for another developer to understand. The key idea is not to prefer closures everywhere; it is to understand what binding nonlocal changes, so you can choose a small, correct tool when it fits. These assertions make the independence claim testable. Counter A starts at five and counter B at twenty. We alternate calls, including a custom step of three, a zero step, and a negative step. Each expected value depends only on that counter's own previous value. If a future change accidentally moves count to a global variable, one of these
- 04:53
assertions will fail even if a single counter still appears correct. The test also makes our contract clear: zero and negative steps are permitted. Another application might forbid negative steps; it could validate that inside advance and test the rejection separately. In the matching browser task, write the inner function yourself and watch the checked task call two counters in alternating order. If you see UnboundLocalError, check the location of nonlocal. If every call returns one, look for a count initialized inside advance instead of the factory. If two counters interfere, look for global state.
- 05:33
After you pass the task and quiz, try returning two closures from one factory: one to increment and one to read. Ask whether they should share the same captured count, then verify the behavior with a short assertion.
Understand the concept
A closure is a function that keeps access to variables from an enclosing function after that outer call has returned. `make_counter` can create a new count and return an `advance` function; every call to that returned function sees the count captured by its own factory call. Closures are useful for small pieces of private state, callbacks, and configurable behavior. They are not automatically shared global variables.
Reading a captured name works without a declaration. Rebinding it is different. If Python sees `count += step` inside `advance`, it treats count as a local name for that function. The right-hand read then occurs before the local has a value, raising UnboundLocalError. Add `nonlocal count` to say that assignment targets the binding in the nearest enclosing function scope. `nonlocal` cannot create a new name; it must refer to an existing binding in an enclosing function.
A closure can also mutate an object it captured without rebinding the name. For example, `items.append(value)` changes a captured list and does not require nonlocal. That distinction helps diagnose when the keyword is truly needed. Avoid hiding complicated state machines inside layers of closures; a small class may be clearer when there are several operations or invariants.
The task creates counters with independent state. Each returned `advance` accepts a step, defaults to one, adds it to the captured count, and returns the new value. An explicit zero step leaves the value unchanged; negative steps are allowed. Hidden checks call two counters in alternating order, verify default and custom steps, and check that one counter cannot change the other's state. First run the broken function in the guide and read the exact exception name, then add nonlocal and verify the separate instances.
- A returned inner function retains enclosing bindings
- Assignment makes a name local unless nonlocal is declared
- Each factory call creates independent captured state
- Mutating an object and rebinding a name are different operations
See it step by step
Read the code, predict the output, then compare it with the result.
01. Read a captured value
def make_greeting(prefix):
def greet(name):
return f"{prefix}, {name}"
return greet
hello = make_greeting("Hello")
print(hello("Ada"))Hello, AdaThe returned function still reads prefix after make_greeting has finished.
02. Reproduce the missing nonlocal declaration
def broken_counter():
count = 0
def advance():
count += 1
return count
return advance
try:
broken_counter()()
except UnboundLocalError as error:
print(type(error).__name__)UnboundLocalErrorThe assignment makes count local to advance, so the read in += happens too early.
03. Give each counter its own binding
def make_counter(start=0):
count = start
def advance(step=1):
nonlocal count
count += step
return count
return advance
a = make_counter()
b = make_counter(10)
print(a(), a(2), b(), a())1 3 11 4nonlocal updates the enclosing count; a and b came from separate factory calls.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Make the captured count fail visibly
The returned advance function seems to have access to count from make_counter. That is true when it only reads count. But `count += 1` is an assignment in advance. Python decides count is local to the inner function, then tries to read that still-unbound local on the right-hand side. The first call raises UnboundLocalError.
Catching and printing the exception name keeps this teaching sample runnable. The mistake is not that the outer function returned; closures are designed to retain access to enclosing variables. The missing piece is a declaration about which binding the assignment should update. Do not move count to a module global, because separate counters would then share one value.
def broken_counter():
count = 0
def advance():
count += 1
return count
return advance
try:
broken_counter()()
except UnboundLocalError as error:
print(type(error).__name__)UnboundLocalError- Run the returned function, not just the factory.
- Locate the `+=` line that reads the local before any local value exists.
Declare the enclosing binding with nonlocal
Add `nonlocal count` inside advance. It tells Python that the assignment targets the nearest enclosing function's count binding. The declaration must refer to a binding that already exists in an enclosing function; it cannot create an unrelated name. Each call to make_counter creates a new count and a new returned function that closes over it.
The first counter starts at zero; the second starts at ten. Interleaving calls proves they do not share one global value. The optional step is just a parameter to advance, so passing two increments by two and passing zero leaves the count unchanged. The result of each call is the current state, not a copy of a global counter.
def make_counter(start=0):
count = start
def advance(step=1):
nonlocal count
count += step
return count
return advance
a = make_counter()
b = make_counter(10)
print(a(), a(2), b(), a(0), b())1 3 11 3 12- Point to the nonlocal declaration before the update.
- Alternate the two counters and confirm their histories remain separate.
Separate rebinding from object mutation
A captured list can be mutated without rebinding the variable that refers to it. `items.append` looks up items and changes that list object. The function does not assign a new object to the name items, so it does not need a nonlocal declaration. In contrast, `items = items + [value]` would rebind items and would need nonlocal if the enclosing binding should change.
Use this distinction when diagnosing a closure. Sometimes a short list-collecting closure is clear; sometimes a class with explicit methods is easier to maintain. For the task, keep the simple integer counter, because integer addition always creates a new integer and necessarily rebinds count. Test two factory calls and a negative step to verify the behavior contract.
def make_collector():
items = []
def add(value):
items.append(value)
return list(items)
return add
first = make_collector()
second = make_collector()
print(first("red"))
print(first("blue"))
print(second("green"))['red']
['red', 'blue']
['green']- Notice there is no assignment to the name items inside add.
- Verify the second collector has its own list rather than sharing the first collector's list.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Declare `nonlocal count` at the start of advance, then assign count += step and return count. The declaration belongs in the inner function, not make_counter.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Rebinding without nonlocal
def advance():
count += 1def advance():
nonlocal count
count += 1What happens: UnboundLocalError appears on the first call.
The declaration targets the enclosing function's binding.
Moving state to a global
global count
count += 1nonlocal count
count += 1What happens: All counters affect the same module-level value.
Keep each counter's state inside its own factory call.