Closures and Variable Scoping in Python

· 5 min read · Updated March 12, 2026 · intermediate
python intermediate scoping

If you’ve written any moderately complex Python code, you’ve probably encountered closures—even if you didn’t realize it. Closures are functions that remember the variables from their enclosing scope, even after that outer function has finished executing. They’re the secret sauce behind decorators, function factories, and callbacks. Understanding closures will level up your Python game.

What is a Closure?

A closure is a function that captures variables from its lexical environment. In plain English: it’s a function defined inside another function that keeps access to the outer function’s variables, even after the outer function returns.

Here’s the simplest example:

def outer():
    message = "Hello from outer!"
    
    def inner():
        print(message)
    
    return inner

closure = outer()
closure()  # Prints: Hello from outer!

The inner function is a closure. It “closes over” the message variable from its enclosing scope. When we call closure() later, it still has access to message, even though outer() has already finished running.

Why does this work? Python keeps a reference to the enclosing scope’s variables in something called the closure’s __closure__ attribute. You can verify this:

def outer(x):
    def inner():
        return x
    return inner

f = outer(10)
print(f.__closure__)  # (<cell at ...: int object at ...>,)
print(f.__code__.co_freevars)  # ('x',)
print(f())  # 10

The co_freevars tuple shows which variables the closure captures. The __closure__ contains the actual values.

Variable Scope in Python

Before diving deeper into closures, you need to understand how Python finds variables. Python follows the LEGB rule—the order in which it searches for variable names:

  1. Local (inside the current function)
  2. Enclosing (functions containing the current function)
  3. Global (module level)
  4. Built-in (Python’s built-in names like print, len)

When you reference a variable, Python checks each scope in order. Assignment creates a new local variable unless you explicitly use global or nonlocal.

The global Keyword

The global keyword lets you write to module-level variables from inside a function:

counter = 0

def increment():
    global counter
    counter += 1

increment()
print(counter)  # 1

Without global, Python would create a new local counter, and the original would stay 0.

The nonlocal Keyword

The nonlocal keyword is specifically for closures. It lets you modify variables from the enclosing function’s scope:

def counter():
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        return count
    
    return increment

c = counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

Without nonlocal, you’d get an UnboundLocalError because Python would see count += 1 as an assignment, making count local—and then you’d be reading it before assignment.

Practical Examples

Function Factory

Closures shine when you need to create specialized functions on the fly:

def power_factory(exp):
    def power(base):
        return base ** exp
    return power

square = power_factory(2)
cube = power_factory(3)

print(square(5))  # 25
print(cube(5))    # 125

Each returned function remembers its own exp value. This is cleaner than passing the exponent every time you call the function.

Decorators (Under the Hood)

Decorators are just closures in action. Here’s a simple one:

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

@logger
def greet(name):
    return f"Hello, {name}!"

greet("World")

The wrapper function is a closure over func. It captures the original function and adds behavior around it.

Callback Functions

In event-driven programming or asynchronous code, closures commonly serve as callbacks:

def make_handler(message):
    def handler(event):
        print(f"{message}: {event}")
    return handler

click_handler = make_handler("Button clicked")
hover_handler = make_handler("Mouse hovered")

Each handler remembers its specific message. This pattern appears everywhere in GUI programming, web frameworks, and async libraries.

Common Pitfalls

The Loop Closure Bug

A classic mistake when creating multiple closures in a loop:

funcs = []
for i in range(3):
    funcs.append(lambda: i)

for f in funcs:
    print(f())  # Prints: 2, 2, 2 (not 0, 1, 2!)

All lambdas capture the same i variable. By the time you call them, the loop has finished and i is 2. The fix is to capture the value at definition time:

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # Default argument captures current value

for f in funcs:
    print(f())  # Prints: 0, 1, 2

Accidental Closure Creation

Every nested function creates a closure, even if you don’t use it:

def outer():
    x = 10
    
    def inner():
        pass  # x is never used, but still captured
    
    return inner

This has a small memory cost—the closure holds a reference to x. Usually fine, but worth knowing if you’re creating thousands of functions.

Mutable Default Arguments

This isn’t strictly about closures, but it’s a related gotcha:

def append_to(element, to=[]):
    to.append(element)
    return to

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] — not [2]!

The default list persists across calls. Use None instead:

def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

See Also

Conclusion

Closures are everywhere in Python once you know what to look for. They let functions carry their context with them, enabling patterns like decorators, function factories, and callbacks. The key is understanding what Python captures and when. Remember the LEGB rule for scope lookup, use nonlocal when you need to modify enclosed variables, and watch out for the loop closure bug when creating multiple functions in loops. Once closures click, you’ll see them everywhere—and wonder how you coded without them.