Skip to Content
Python Closures: Building a Counter Without a Global Variable
Advanced
0 likes
Tutorials

Python Closures: Building a Counter Without a Global Variable

A closure remembers variables from the function that created it. Build separate counters with no globals, fix the UnboundLocalError with nonlocal, avoid the lambda-in-a-loop trap, and know when a class is clearer.

A closure is a function that remembers variables from the function that created it, even after that outer function has finished. The classic first use is a counter that doesn't need a global variable. It's a small example, but it shows the one rule that trips everyone up: reading an outer variable just works, and changing it needs nonlocal.

Skip ahead: If you know what a closure is and only hit UnboundLocalError, go to "Changing the remembered value". If you're deciding between a closure and a class, go to "Closure or class?".

The problem with a global counter

count = 0


def next_ticket():
    global count
    count += 1
    return count


print(next_ticket(), next_ticket(), next_ticket())
count = 0
print(next_ticket())
1 2 3
1

It works, but count belongs to the whole file. Any code can reset it, like the count = 0 line above, and you can only have one counter. If you need one for the kitchen queue and one for the cashier queue, you're copying functions and globals.

A function that makes counters

def make_counter():
    count = 0

    def next_number():
        nonlocal count
        count += 1
        return count

    return next_number


kitchen = make_counter()
cashier = make_counter()

print(kitchen(), kitchen(), kitchen())
print(cashier())
print(kitchen())
1 2 3
1
4

make_counter() runs, creates count, defines next_number, and returns it. Normally count would be gone once make_counter finishes. But next_number still uses it, so Python keeps it alive, attached to that function. Each call to make_counter() creates a new count, so kitchen and cashier are fully separate. And no code outside can reach count to reset it by accident.

You can see where it's stored, if you're curious:

def make_counter():
    count = 0

    def next_number():
        nonlocal count
        count += 1
        return count

    return next_number


kitchen = make_counter()
kitchen()
kitchen()
print(kitchen.__code__.co_freevars)
print(kitchen.__closure__[0].cell_contents)
('count',)
2

Changing the remembered value

Remove the nonlocal line and run it:

def make_counter():
    count = 0

    def next_number():
        count += 1
        return count

    return next_number


kitchen = make_counter()
kitchen()
Traceback (most recent call last):
  File "/home/you/project/broken.py", line 12, in <module>
    kitchen()
    ~~~~~~~^^
  File "/home/you/project/broken.py", line 5, in next_number
    count += 1
    ^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Python decides whether a name is local when it reads the function, not when it runs it. count += 1 assigns to count, so Python treats count as a new local variable inside next_number. Then count + 1 tries to read that local before it has a value. The error says exactly that.

nonlocal count tells Python: this count is the one from the enclosing function, not a new local. Two rules:

  • Reading an outer variable never needs nonlocal.
  • Assigning to it (=, +=) always does.

💡 Tip: Changing what's inside a list or dictionary isn't assigning to the variable. items.append(x) in an inner function works without nonlocal, because items still points to the same list. That's why older code (before Python 3 added nonlocal) used count = [0] and count[0] += 1. You don't need that trick anymore.

A more useful counter

Same idea, with a starting number and a step, plus a second function sharing the same remembered value:

def make_ticket_counter(prefix, start=1):
    number = start - 1

    def next_ticket():
        nonlocal number
        number += 1
        return f"{prefix}-{number:03d}"

    def peek():
        return number

    return next_ticket, peek


take, last = make_ticket_counter("K", start=41)
print(take())
print(take())
print("last issued:", last())
K-041
K-042
last issued: 42

next_ticket and peek were created in the same call, so they share the same number. peek only reads it, so it doesn't need nonlocal.

The loop trap

Closures remember the variable, not the value it had when the inner function was made:

buttons = []
for size in ["S", "M", "L"]:
    buttons.append(lambda: f"Size {size}")

print([button() for button in buttons])

buttons = []
for size in ["S", "M", "L"]:
    buttons.append(lambda size=size: f"Size {size}")

print([button() for button in buttons])
['Size L', 'Size L', 'Size L']
['Size S', 'Size M', 'Size L']

In the first loop, all three functions look up size when they're called, after the loop has finished, so they all see "L". The fix in the second loop uses a default argument, which is evaluated once, when each function is created. That's one of the few places where "defaults are created once" is exactly what you want.

Closure or class?

A closure is a small object with one method. When it grows, switch to a class:

class Counter:
    def __init__(self, start=0):
        self.count = start

    def __call__(self):
        self.count += 1
        return self.count

    def reset(self):
        self.count = 0


kitchen = Counter()
print(kitchen(), kitchen())
kitchen.reset()
print(kitchen())
1 2
1

My rule: one action, maybe two, and nothing outside needs to see the state? Closure. Several actions, like reset, peek and history, or other code needs to read the value? Class. The class is a few lines longer and much easier for the next person to read once it grows. Neither one is more "advanced". They're two tools for the same job at different sizes.

Where you'll meet closures

  • Decorators. The wrapper function remembers the function it wraps. That's a closure.
  • Callbacks. A button handler that remembers which item it belongs to.
  • Functions that make functions. make_vat(0.12) returning a function that adds 12%.

Common mistakes

  • **Forgetting nonlocal.** UnboundLocalError the first time you call it.
  • **Using global instead.** It skips the enclosing function and goes straight to the module level. That's a different variable.
  • Lambdas in a loop. They all see the last value. Use a default argument.
  • A closure with five inner functions. That's a class pretending not to be one.

What happens if you stick with globals

One counter works. The second one copies the first. Then a test resets the global and another test fails because it expected 3 and got 1. The bug isn't in either test. It's in the shared state both of them touched.

If two parts of your program both call next_ticket(), should they share one sequence of numbers or have their own, and does your code make that choice clear?

About

How closures keep state, why assignment needs nonlocal, two functions sharing one value, the late-binding loop trap, and closure vs class. After this, you can build small stateful functions and read decorator code more easily.

Responsible
Mary Q
Last Update
09/12/2026
Reading Time
6 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.