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:

  1. Local — current function's scope
  2. Enclosing — outer function's scope (if any)
  3. Global — module-level scope
  4. 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:

  1. A nested function exists (function inside a function)
  2. The inner function references variables from the outer function's scope
  3. 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 nonlocal sparingly: Only when you need to reassign variables from the enclosing scope.
  • Combine closures with functools.partial: For simple partial function cases, functools.partial may 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: