A closure is a special kind of function in Python that remembers values from the environment where it was created—even if that environment no longer exists. In other words, a closure “closes over” variables from its outer function and keeps them safe inside.
Why does this matter? Closures help you keep data private and create flexible, reusable functions. They allow you to build small building blocks that carry their own memory without needing extra objects or global variables.
Here’s a friendly example: imagine a function that creates a greeter for a specific name. The returned function remembers that name and can greet anyone with it:
def make_greeter(name):
def greet():
return f"Hello, {name}!"
return greet
greeter_for_moody = make_greeter("Moody")
print(greeter_for_moody())
Even though make_greeter
has finished running, the greet
function it returned still remembers the name "Moody"
—that’s a closure in action!
This lets you create personalized functions that keep their own little world of data.
Creating Closures: Basic Example
Creating a closure in Python is simple. You write a function that returns another function, and the inner function uses a variable from the outer one. The inner function keeps a memory of that variable, even after the outer function finishes.
Let’s try a fun example with animals. Imagine you want to make a greeter that remembers an animal’s name and says hello to it.
def animal_greeter(animal_name):
def greet():
return f"Hello, {animal_name}!"
return greet
lion_greeter = animal_greeter("Lion")
print(lion_greeter())
Here’s what happens: animal_greeter("Lion")
creates a new function called greet
that remembers the name "Lion"
. When you call lion_greeter()
, it uses that saved name to say hello.
This is a closure—the inner function greet
keeps the animal_name
safe inside its memory, even after animal_greeter
has finished running.
Closures let you build small, personalized functions like this all over your code.
Using Closures to Keep State
Closures are great for storing information quietly inside a function—without needing global variables or creating classes. This means you can keep track of changing data in a neat, simple way.
For example, let’s build a counter that remembers how many times it’s been called. Each time you call it, the count goes up by one:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return f"Count is {count}"
return counter
my_counter = make_counter()
print(my_counter()) # Count is 1
print(my_counter()) # Count is 2
print(my_counter()) # Count is 3
Here’s how it works: make_counter()
creates a count
variable set to 0. The inner function counter()
increases count
by 1 every time it runs and remembers the updated value because of the closure.
The keyword nonlocal
lets the inner function change the count
variable from the outer function.
This way, the counter keeps its own private state without polluting the rest of your program. Closures make it easy to create functions with memory, all without the fuss of classes or global variables.
Closures vs. Regular Nested Functions
Not all nested functions are closures. The key difference is that closures remember the variables from the outer function even after that outer function has finished running. Regular nested functions, on the other hand, only use outer variables while they are still active.
Let’s look at an example to understand this better:
def outer_func(x):
def inner_func():
return x + 5
return inner_func
my_func = outer_func(10)
print(my_func()) # Output: 15
Here, inner_func
uses the variable x
from outer_func
. Even though outer_func
has finished running, my_func()
still remembers x = 10
and adds 5 to it. This is a closure in action — the inner function keeps the value of x
alive.
Now, consider this example without returning the inner function:
def outer_func(x):
def inner_func():
return x + 5
print(inner_func())
outer_func(10)
In this case, inner_func
is called inside outer_func
while x
is still active. But once outer_func
ends, inner_func
disappears too, so there is no closure.
To sum up, a closure is a nested function that retains access to the outer function’s variables even after the outer function has finished. This lets you create powerful, memory-carrying functions that live on by themselves.
Modifying Outer Variables: The nonlocal
Keyword
By default, variables in the outer function are read-only inside a nested function. If you want to change those variables from within the inner function, Python gives you the nonlocal
keyword. It tells Python, “Hey, use the variable from the outer scope, not create a new one.”
Let’s improve our counter example to show how nonlocal
works:
def make_counter():
count = 0 # This is the outer variable
def counter():
nonlocal count # Tell Python to use the outer 'count' variable
count += 1
return f"Count is {count}"
return counter
my_counter = make_counter()
print(my_counter()) # Count is 1
print(my_counter()) # Count is 2
print(my_counter()) # Count is 3
Without nonlocal
, the inner function would create a new count
variable each time, and the count would never increase properly.
Using nonlocal
lets the inner function update the outer variable, so the counter remembers its current value and counts up correctly.
Closures combined with nonlocal
let you create neat, stateful functions without classes or global variables.
Practical Examples with Closures
Closures are great tools to build small, reusable pieces of code that remember important details. Let’s see a few practical ways you can use them.
One popular use is personalized greetings. You can create a greeter function that remembers a name and returns custom greetings whenever called:
def make_greeter(name):
def greet():
return f"Hello, {name}!"
return greet
greeter = make_greeter("Harry")
print(greeter())
Closures are also handy for simple calculators, like adders or multipliers. Here’s an adder that remembers a number and adds it to any input:
def make_adder(n):
def add(x):
return x + n
return add
add_five = make_adder(5)
print(add_five(10)) # 15
Finally, closures can help you create custom message formatters that attach prefixes or suffixes to messages:
def make_formatter(prefix, suffix):
def format_message(msg):
return f"{prefix}{msg}{suffix}"
return format_message
excited = make_formatter("Wow! ", "!!!")
print(excited("Python is fun")) # Wow! Python is fun!!!
These examples show how closures help build flexible, easy-to-use functions that keep their own little memories. You can mix and match these ideas to fit your projects, whether you’re making games, tools, or fun scripts.
Closures with Lambdas
Closures aren’t limited to regular def
functions. You can also create them using lambda
— Python’s way of writing small, anonymous functions in one line. Lambdas are especially useful for quick tasks where you want to return a function that remembers a value.
Let’s look at a simple, curried greeting built with lambdas:
make_greeter = lambda name: (
lambda message: f"{name} says: {message}"
)
greeter = make_greeter("Lucia")
print(greeter("Good morning!")) # Lucia says: Good morning!
make_greeter("Lucia")
returns a new function that still remembers "Lucia"
. That inner lambda then uses the name when it’s finally given a message.
You get the same closure behavior as with def
, but in a quick, tidy format. Just keep lambda closures simple — if your logic starts getting long or tricky, it’s better to switch back to regular functions for clarity.
Using Closures with Higher-Order Functions
Closures are a perfect match for higher-order functions like map()
, filter()
, and sorted()
— tools that take other functions as input. When you use closures with them, you can create flexible filters, mappers, or sort keys that remember extra details without writing full-blown classes or extra logic.
Let’s say you want to filter a list of numbers, keeping only those above a certain threshold. Instead of hardcoding the limit, you can create a closure that remembers the threshold:
def make_filter(threshold):
def above_limit(number):
return number > threshold
return above_limit
numbers = [5, 12, 8, 20, 3]
filter_high = make_filter(10)
print(list(filter(filter_high, numbers))) # [12, 20]
Here, make_filter(10)
gives us a function that checks if a number is greater than 10. That function is then passed into filter()
, which applies it to each item in the list.
You can use this same idea with map()
to transform values using remembered settings, or with sorted()
by defining a custom sort key that holds extra information.
Closures make your code feel like it’s speaking a mini-language of its own — focused, reusable, and expressive.
Tips for Writing Clear Closures
Closures are powerful, but like any tool, they’re best used with care. To keep your closures easy to understand, try to keep them short and focused. If a closure grows too big or handles too many tasks, it might be better to use a class or regular function with parameters.
Always give your inner function a clear name that matches what it does. For example, if your closure checks if a number is large enough, name it something like is_large_enough
, not just check
or func
. This helps others — and your future self — understand what your code is doing at a glance.
Also, avoid deep or confusing layers of nesting. Two levels (an outer and an inner function) are usually enough. If you find yourself nesting even more functions inside, it might be a sign to restructure your code.
Lastly, use closures where they truly make things cleaner — like storing state, customizing behavior, or creating reusable helpers. When used well, closures make your Python code more elegant and expressive without adding extra weight.
Closing Example: A Mini Zoo Keeper with Closures
Let’s wrap up with a fun example that brings everything together: closures, remembered state, nonlocal
, and personalization. Imagine we’re running a small zoo, and we want to create “zoo keepers” — little helpers who remember their favorite animal and what it’s been up to.
We’ll build a function that returns a personalized zoo keeper. Each one remembers its animal’s name and can update or report the animal’s last action.
def zoo_keeper(animal):
action = "sleeping"
def keeper(command, new_action=None):
nonlocal action
if command == "update" and new_action:
action = new_action
return f"{animal} is now {action}."
elif command == "report":
return f"{animal} is currently {action}."
else:
return "Unknown command."
return keeper
leo_keeper = zoo_keeper("Lion")
print(leo_keeper("report")) # Lion is currently sleeping.
print(leo_keeper("update", "roaring")) # Lion is now roaring.
print(leo_keeper("report")) # Lion is currently roaring.
Each call to zoo_keeper()
gives you a new, memory-holding function. leo_keeper
remembers the lion and keeps track of what it’s doing — without needing a class or global variable.
This is the magic of closures: they let you package up behavior and memory into a neat little function. Whether you’re managing animals, customizing messages, or tracking user preferences, closures let you create smart helpers that remember what matters.
Conclusion
Closures in Python let you write tidy, self-contained functions that remember things — even after their outer function has finished running. This makes them perfect for building smart, reusable tools that quietly hold onto state without cluttering your code with globals or extra objects.
Whether you’re making animal keepers, counting functions, or personalized greetings, closures help you wrap behavior and memory into a single, flexible package. They’re simple, powerful, and fun once you get the hang of them.
So go ahead — try them out in your own projects. Play with creative ideas, use closures to keep your code clean, and bring a little life to your functions.