Python Closures

In the last lesson you learned about Decorators. They felt a little bit like magic, right? A function wrapping another function and remembering things. Now we pull back the curtain. The idea that makes decorators work has a name. It is called a closure. Once you get it, decorators stop feeling like magic and start feeling obvious.

🤔 Why Closures?

Here is the pain. Sometimes you want a function that already has a value built into it. Say you want a function that always multiplies by 3. And another that always multiplies by 10. You could write two separate functions by hand. But that gets boring fast. And it does not work well if the number only shows up while the program is running.

A closure fixes this. You write one factory function. You tell it the number once. It hands you back a small function that remembers that number forever.

🧩 What Is a Closure?

A closure is an inner function that remembers values from the outer function where it was created. It keeps those values even after the outer function has finished running.

Read that last part again. Even after the outer function has finished. Normally when a function returns, all its local variables are gone. Python cleans them up. But if an inner function still needs one of them, Python keeps it alive just for that inner function. That kept-alive value is what makes it a closure.

Think of it like a lunchbox. In the morning Riya packs a lunchbox at home. Then she leaves the house. The kitchen is closed and empty now. But she still has the food. She carried it with her in the box. The inner function is Riya. The lunchbox is the closure. The food inside is the remembered value from the outer function.

🛠️ The Syntax

A closure needs three things to exist:

  • An outer function that holds a variable.
  • An inner function defined inside it that uses that variable.
  • The outer function returning the inner function. Returning it, not calling it.

Here is the smallest possible closure. The outer function holds a greeting. The inner function uses it.

def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet # return the function itself, no parentheses
say_hello = make_greeter("Hello")
say_hi = make_greeter("Hi")
print(say_hello("Alex"))
print(say_hi("Riya"))

Output

Hello, Alex!
Hi, Riya!

Let us walk through what just happened, line by line.

  • make_greeter("Hello") runs and reaches return greet. At this point make_greeter is finished.
  • Normally greeting would now disappear. But greet still needs it. So Python keeps "Hello" attached to that returned function.
  • We saved that returned function in say_hello. So say_hello is a small function that secretly carries "Hello" inside it.
  • say_hi carries "Hi" in its own separate box. The two do not share. Each closure keeps its own copy.

That is the key insight. Notice we wrote return greet with no parentheses. We are returning the function object itself, not the result of calling it. If you wrote return greet() you would get an error. That is because greet needs a name and we have not given one yet.

🔢 The Multiplier Example

This is the example you will see everywhere. So let us build it properly. We want a factory that makes multiplier functions. You give it a number n. It gives you back a function that multiplies anything by n.

The code is short. Read it first, then we will break it down.

def make_multiplier(n):
def multiply(x):
return x * n # n is remembered from the outer function
return multiply
times_three = make_multiplier(3)
times_ten = make_multiplier(10)
print(times_three(5)) # 5 * 3
print(times_ten(5)) # 5 * 10
print(times_three(100)) # 100 * 3

Output

15
50
300

Here is what each piece is doing:

  • make_multiplier(3) runs, defines multiply, and returns it. The 3 gets remembered inside that returned function.
  • times_three is now a function that means “multiply by 3”. It never forgets its 3.
  • make_multiplier(10) does the same thing but remembers 10 instead. So times_ten means “multiply by 10”.
  • When we call times_three(5), Python looks for x (that is 5) and n (that is the remembered 3), and gives back 15.

See how the two functions keep their own remembered value? times_three carries a 3. times_ten carries a 10. They came from the same factory but they do not get in each other’s way. Each one got its own lunchbox.

Tip

Want to peek inside a closure and see what it remembered? Every closure stores its kept values in a hidden spot you can inspect with __closure__. Try print(times_three.__closure__[0].cell_contents) and Python prints 3. That is proof the value is really being carried around.

🎁 Closures Are the Idea Behind Decorators

Remember decorators from the last lesson? A decorator takes a function, wraps it in another function, and returns the wrapper. That wrapper needs to remember the original function so it can call it later. Remembering something after the outer function finished is exactly what a closure does.

Look at this tiny decorator. The part that “remembers” the original func is a closure.

def announce(func):
def wrapper():
print("About to run the function...")
func() # func is remembered through the closure
print("Done running it.")
return wrapper
@announce
def say_hi():
print("Hi there!")
say_hi()

Output

About to run the function...
Hi there!
Done running it.

The wrapper function uses func even though announce already finished. That only works because of a closure. So every decorator you write is quietly using the closure idea you just learned. So a decorator is really just a closure that wraps another function.

⚠️ Common Mistakes

A few things trip people up the first time. Watch out for these.

  • Returning the call instead of the function. Writing return greet() runs greet right now and returns its result. You wanted to hand back the function so you could call it later.
# ❌ Avoid: this calls greet right away and returns its result (and errors)
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet() # oops, parentheses
# ✅ Good: return the function object, no parentheses
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet
  • Trying to change the remembered variable. A plain inner function can read the outer variable but not reassign it. If you try, Python thinks you mean a new local variable.
# ❌ Avoid: this raises an error because Python treats count as local
def make_counter():
count = 0
def increment():
count = count + 1 # error: count used before assignment
return count
return increment
# ✅ Good: tell Python you mean the outer count with nonlocal
def make_counter():
count = 0
def increment():
nonlocal count
count = count + 1
return count
return increment
  • The loop variable surprise. If you build closures inside a loop and they all use the loop variable, they may all end up sharing the final value. Bind it as a default argument (def f(x=i)) to give each closure its own copy.

✅ Best Practices

Keep these habits and closures stay simple and friendly.

  • Name the factory after what it builds. make_multiplier, make_greeter, build_validator. The name should tell the reader they get a function back.
  • Keep the inner function small. A closure is best when the inner function does one clear job with the remembered value.
  • Reach for nonlocal only when you truly need to change the remembered value, like a counter. If you are only reading it, you do not need nonlocal at all.
  • If a closure starts holding lots of state, a class might be clearer. A closure is great for one or two remembered values, not a whole pile.

🧩 What You’ve Learned

You now understand the idea that powers decorators. Quick recap.

  • ✅ A closure is an inner function that remembers values from its outer function, even after that outer function has finished.
  • ✅ You build one with an outer factory function, an inner function that uses an outer variable, and a return of the inner function with no parentheses.
  • make_multiplier(n) returns a function that always multiplies by that n, and each returned function keeps its own remembered value.
  • ✅ Decorators work because of closures. The wrapper remembers the original function after the outer function finished.
  • ✅ Use nonlocal when the inner function needs to change the remembered value, not just read it.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What makes an inner function a closure?

    Why: A closure is an inner function that keeps using variables from the outer function even after the outer function returned.

  2. 2

    In make_multiplier(n), why do you write 'return multiply' and not 'return multiply()'?

    Why: Without parentheses you return the function object so it can be called later; with parentheses you would call it now and return its result.

  3. 3

    If times_three = make_multiplier(3) and times_ten = make_multiplier(10), what do they share?

    Why: Each call to the factory creates a separate closure, so times_three carries 3 and times_ten carries 10 independently.

  4. 4

    Why do decorators rely on closures?

    Why: A decorator's wrapper keeps using the original function after the decorator function returned, which is exactly what a closure does.

🚀 What’s Next?

You have seen how a function can remember state and carry behavior around. Next we use a similar setup trick to manage resources cleanly. Think of files and connections that must open and then always close.

Custom Context Managers

Share & Connect