Advanced PythonAdvanced10 min57 / 63

Closures

Discover how a nested function can remember variables from its surroundings long after the outer function has finished running.

Imagine you order a custom stamp at a print shop. You hand the clerk your initials, they carve the stamp, and then they hand it back to you. From that moment on, the stamp remembers your initials — even though the clerk and the print shop are long gone.

A closure works the same way. You call a function, it builds and returns a brand-new inner function, and that inner function remembers variables from the outer function — even after the outer function has finished running. This sounds magical at first, but it is one of Python's most useful and elegant tools.

See it in action

Visual walkthrough1 / 5
1

Functions That Remember

A closure is an inner function that remembers variables from its outer function — even after that outer function has finished running. Think of it as a function carrying a little backpack of values wherever it goes.

Closures are the secret engine behind decorators and factory functions.

#Functions Inside Functions

Before we talk about closures, let's get comfortable with the idea that functions can live inside other functions. Python allows this, and it is perfectly normal.

inner() can read 'message' even though it was defined in outer().
def outer():
    message = "Hello from outer!"

    def inner():
        print(message)  # inner can see outer's variable

    inner()  # call inner from inside outer

outer()

So far, inner runs inside outer. That is neat, but not yet a closure. A closure happens when we return the inner function so it can be used outside — after outer has already finished.

#Your First Closure

Each call to make_greeter() creates a fresh closure that remembers its own 'name'.
def make_greeter(name):
    def greet():
        print(f"Hey, {name}!")
    return greet  # return the function itself, not greet()

say_hi = make_greeter("Alice")
say_hi()   # make_greeter is long gone — but name is remembered!

say_bye = make_greeter("Bob")
say_bye()
Think of it like

A closure is a backpack

When greet is created, Python quietly packs up the variables it needs — like name — into a little backpack. Wherever greet travels, the backpack comes with it. This backpack is the closure itself.

You can even peek inside: say_hi.__closure__[0].cell_contents returns 'Alice' — the value Python kept alive.

#Factory Functions — Making Configurable Functions

One of the most common uses of closures is building factory functions — functions that manufacture other functions, each pre-configured with a specific setting.

Here is a classic example: a multiplier factory.

double and triple are separate closures, each remembering a different value of n.
def make_multiplier(n):
    def multiply(x):
        return x * n   # n is captured from the enclosing scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 5 * 2
print(triple(5))   # 5 * 3
print(double(9))   # 9 * 2

Notice we only write the multiplication logic once. Then we configure it differently by calling make_multiplier with different values. This keeps code DRY — Don't Repeat Yourself.

#State Without a Class — Using nonlocal

Closures can not only read captured variables — they can also update them, keeping state between calls. To do this, you need the nonlocal keyword, which tells Python: "I want to modify the variable from the outer scope, not create a brand-new local one."

The closure remembers count across calls, acting like a lightweight object with state.
def make_counter():
    count = 0

    def increment():
        nonlocal count   # without this, count would be a new local variable
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3
Common mistake

Forgetting nonlocal causes an UnboundLocalError

If you write count += 1 without nonlocal count, Python sees the assignment and decides count is a local variable — but you never gave it a local value, so it crashes with UnboundLocalError: local variable 'count' referenced before assignment.

Rule of thumb: any time you assign to a captured variable (not just read it), you need nonlocal.

#Why Are Closures Useful?

Closures shine in several real-world situations:

  • Configurable behaviormake_multiplier, make_validator, make_logger. One template, many specializations.
  • Lightweight state — A counter, an accumulator, or a cache without writing a full class.
  • Callbacks and event handlers — Pass a closure to a button click handler that already "knows" which item to act on.
  • Building decorators — Decorators (the @ syntax you may have seen) are implemented using closures under the hood.

#The Connection to Decorators

Decorators are one of the most famous uses of closures. When you write @timer above a function, Python is really doing this:

``python my_function = timer(my_function) ``

timer is a function that accepts another function, wraps it in an inner function (a closure), and returns the wrapper. The wrapper closes over the original function.

The decorator 'shout' works because wrapper is a closure that captures 'func'.
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)  # func is captured from shout's scope
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("world"))
Tip

Closures are the engine inside decorators

You do not need to master decorators to use closures, but knowing closures makes decorators feel obvious rather than magical. Once you are comfortable with make_multiplier, a decorator is just the same idea — a function that takes a function and returns a new, enhanced one.

#A Practical Example — Making a Validator

Two different closures, each remembering different min/max boundaries.
def make_range_checker(min_val, max_val):
    def check(value):
        if min_val <= value <= max_val:
            return f"{value} is valid"
        return f"{value} is OUT OF RANGE ({min_val}–{max_val})"
    return check

check_age  = make_range_checker(0, 120)
check_temp = make_range_checker(-50, 60)

print(check_age(25))
print(check_age(200))
print(check_temp(22))
print(check_temp(-99))
Quick check

What will the following code print? ```python def make_adder(x): def add(y): return x + y return add add5 = make_adder(5) print(add5(3)) ```

Key takeaways

  • A closure is an inner function that **remembers variables from its enclosing scope** even after the outer function has finished.
  • Factory functions like `make_multiplier(n)` use closures to create configurable, reusable functions without repeating logic.
  • Use `nonlocal` when you need to **mutate** (not just read) a captured variable inside an inner function.
  • Closures provide lightweight stateful behavior — like a counter — without needing to write a full class.
  • Decorators are built on closures: a function that wraps another function and returns the wrapper is a closure pattern.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

What does this code print?

predict-output
def make_multiplier(n):
    def multiply(x):
        return x * n
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(4))
print(triple(4))
Fix the bug#2

This counter is supposed to print 1, then 2, then 3. What is wrong?

fix-bug
def make_counter():
    count = 0

    def increment():
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())
print(counter())
print(counter())
Fill in the blank#3

Complete the factory function so it returns an inner function that adds n to any number.

def make_adder(n):
    def add(x):
        return x + 
     add

add10 = make_adder(10)
print(add10(5))   # prints 15
Reorder the lines#4

Put these lines in the right order to build a greeting closure and print 'Hey, Maya!'

1
    return greet
2
def make_greeter(name):
3
        print(f"Hey, {name}!")
4
say_hi = make_greeter("Maya")
5
say_hi()
6
    def greet():
Your turn
Practice exercise

Write a factory function called make_power that accepts an exponent n and returns a function that raises any number to that power. Then create square (n=2) and cube (n=3) from it, and test them with a few numbers. As a bonus challenge, add a call counter using nonlocal so each returned function tracks how many times it has been called, and prints the count alongside the result.

Try it live — edit the code and hit Run to execute real Python:

solution.py · editable