If you have ever written a function inside another function in Python and noticed that the inner function "remembered" the outer function's variables even after the outer function finished running, you have encountered a closure. Closures are one of the most elegant and powerful concepts in the language, forming the foundation for advanced topics like decorators, generators, and functional programming.
In this complete guide, you will learn what closures are, how they work under the hood in Python, when and why to use them, and practical examples that will solidify your understanding. We will start with the basics — Python's scope rules — and work our way up to real-world applications you can use in your code today.
Understanding Python's Scope Rules
Before diving into closures, you need to master how Python resolves variable names. Python follows the LEGB rule (Local, Enclosing, Global, Built-in) when looking up variables. When you reference a variable, the interpreter searches in this order:
- Local — current function's scope
- Enclosing — outer function's scope (if any)
- Global — module-level scope
- Built-in — Python's built-in functions and constants
Refer to the official Python documentation on defining functions for more details on how the interpreter manages these scopes.
Here is a simple example illustrating LEGB:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
outer() # Output: local
Each scope has its own x. But what happens when the inner function does not define its own variable?
Nested Functions and Lexical Scoping
Python allows you to define functions inside other functions — these are called nested functions. The inner function has access to the outer function's variables thanks to lexical scoping (also called static scoping).
def outer():
message = "Hello from the enclosing scope!"
def inner():
print(message)
inner()
outer() # Output: Hello from the enclosing scope!
The inner function can access message because it lives in the enclosing scope of outer. This is lexical scoping in action: Python determines variable scope at compile time, not runtime.
For a deeper dive into scope rules, the Real Python tutorial on the LEGB Rule is an excellent supplementary resource.
What Is a Closure?
A closure occurs when an inner function "captures" variables from the outer function's scope and continues to access them even after the outer function has finished executing. In other words, the closure "remembers" the environment where it was created.
Three conditions must be met for a closure to form:
- A nested function exists (function inside a function)
- The inner function references variables from the outer function's scope
- The outer function returns the inner function
Here is the classic example:
def greet(greeting):
def greet_person(name):
return f"{greeting}, {name}!"
return greet_person
hello = greet("Hello")
goodbye = greet("Goodbye")
print(hello("Alice")) # Output: Hello, Alice!
print(goodbye("Bob")) # Output: Goodbye, Bob!
When we call greet("Hello"), the outer function returns the greet_person function. Even after greet has finished executing, the returned function still "remembers" that greeting is "Hello". That is a closure.
How Closures Work Under the Hood
Python exposes the closure through the magic attribute __closure__. Let us inspect it:
print(hello.__closure__)
print(hello.__closure__[0].cell_contents) # Output: Hello
print(goodbye.__closure__[0].cell_contents) # Output: Goodbye
__closure__ is a tuple of cell objects, one for each captured variable. Each cell stores the current value of the variable. This is how Python implements the closure's "environment."
According to the Python data model specification, cell objects are used to store variables from enclosing scopes.
If a function does not capture any external variables, __closure__ is None:
def simple_function():
return 42
print(simple_function.closure) # Output: None
The nonlocal Keyword
By default, when you assign a value to a variable inside a nested function, Python treats it as a local variable to that function. To modify a variable from an enclosing scope, you must use the nonlocal keyword.
def counter():
total = 0
def increment():
nonlocal total
total += 1
return total
return increment
count = counter()
print(count()) # 1
print(count()) # 2
print(count()) # 3
Without nonlocal, you would get an UnboundLocalError when trying to modify total. The PEP 3104 introduced nonlocal in Python 3 to solve exactly this limitation.
Important: nonlocal does not work for global-scope variables — that is what global is for. nonlocal only searches enclosing function scopes.
Practical Use Cases for Closures
Closures are not just an academic concept. They have extremely practical applications in everyday Python development.
1. Function Factories
The greet example we saw is a function factory. You can create variations of the same logic:
def multiplier(factor):
def multiply(number):
return number * factor
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
This pattern is widely used in libraries like Flask, where you can configure specific behaviors through closures.
2. Stateful Counters and Accumulators
Closures let you create functions with private state without using classes:
def make_counter():
count = 0
def count_up():
nonlocal count
count += 1
return count
return count_up
visits = make_counter()
print(visits()) # 1
print(visits()) # 2
print(visits()) # 3
The count variable is encapsulated inside the closure, inaccessible from the outside — it is like a "private attribute" of a function.
3. Memoization (Result Caching)
Closures are excellent for implementing memoization, a pattern that stores results from previous calls to avoid redundant computations:
def memoize(func):
cache = {}
def execute(*args):
if args not in cache:
cache[args] = func(*args)
print(f"Computing... {args}")
return cache[args]
return execute
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Each value is computed only once
For a detailed explanation of how closures relate to decorators, check out our complete Decorators in Python guide.
4. Custom Callbacks
Closures are perfect for creating callbacks that carry additional context:
def configure_logger(level):
def log(message):
print(f"[{level.upper()}] {message}")
return log
info = configure_logger("info")
error = configure_logger("error")
info("System started") # [INFO] System started
error("Connection failed") # [ERROR] Connection failed
Closures vs Classes: Which to Use?
Both closures and classes allow you to create objects with state. The choice depends on the context:
| Criterion | Closure | Class |
|---|---|---|
| Complexity | Low — ideal for one or two variables | High — ideal for multiple methods and attributes |
| Readability | Simple and direct | More verbose but more explicit |
| State | Variables encapsulated in the closure | Public or private attributes |
| Reusability | Limited (single function) | Inheritance and mixins |
A practical rule: if you need only one method with internal state, use a closure. If you need multiple methods or inheritance, use a class. Complement your studies with our guide on Python Functions to better understand when to use each approach.
Common Pitfalls with Closures
Late Binding
The most famous problem with closures in Python is late binding. Consider:
def create_functions():
funcs = []
for i in range(5):
def func():
return i
funcs.append(func)
return funcs
for f in create_functions():
print(f()) # 4 4 4 4 4 (not 0 1 2 3 4!)
All functions return 4 because the closure captures the reference to i, not its value at creation time. When the functions execute (after the loop), i is already 4.
Solution: Use a default argument to capture the value immediately:
def create_functions():
funcs = []
for i in range(5):
def func(value=i): # Captures i immediately
return value
funcs.append(func)
return funcs
for f in create_functions():
print(f()) # 0 1 2 3 4
The official Python FAQ explains this behavior in detail, including why closures behave this way.
Mutable Variables in Closures
Be careful when capturing mutable objects:
def make_accumulator():
items = []
def add(item):
items.append(item)
return items
return add
acc = make_accumulator()
print(acc(1)) # [1]
print(acc(2)) # [1, 2]
This works, but you are modifying a list that belongs to the closure. For beginners, this behavior can be surprising. Always document clearly when a closure modifies mutable objects.
Closures and Decorators
Closures are the foundation of decorators in Python. A decorator is essentially a closure that takes a function and returns a modified version of it:
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} executed in {end - start:.4f}s")
return result
return wrapper
@timer
def heavy_work():
return sum(range(10**6))
heavy_work()
The timer decorator is a closure: it captures the original function (func) and returns it wrapped with additional timing logic.
Performance Considerations
Closures have a small but real performance cost. Accessing variables from enclosing scopes is slightly slower than local variables because Python must traverse the scope chain. However, for 99% of use cases, this difference is negligible.
According to the official Python documentation on the execution model, name resolution follows well-defined rules that guarantee consistency and predictability. If you need maximum performance in inner loops, prefer local variables and avoid closures in critically executed code.
Final Tips and Best Practices
- Keep closures simple: If a closure needs more than 2-3 variables from the outer scope, consider using a class instead.
- Document your closures: Explain which variables are being captured and why.
- Avoid closures in loops: Late binding can cause subtle bugs. Use default arguments to capture values.
- Use
nonlocalsparingly: Only when you need to reassign variables from the enclosing scope. - Combine closures with functools.partial: For simple partial function cases,
functools.partialmay be clearer than a closure.
Conclusion
Closures are a fundamental tool in the Python developer's toolkit. They allow you to create stateful functions, function factories, decorators, and custom callbacks in an elegant and concise way. Understanding closures deeply is essential for mastering advanced topics like asynchronous programming, metaprogramming, and web frameworks.
Now that you understand what closures are, how they work internally through __closure__, and how to apply them in everyday coding, you are ready to write more expressive and efficient Python code. Practice creating your own closures — start with a simple function factory and evolve to more complex patterns like memoization and decorators.
Helpful references for further study: