PYTHON

Python Closure: Syntax, Usage, and Examples

A Python closure is a function that remembers values from the scope where it was created, even after that scope has finished running. In practice, a closure lets a function “carry” some data around with it.


How to Use Closures in Python

A closure appears when three conditions are met:

  1. You define a function inside another function.
  2. The inner function refers to variables from the outer function.
  3. The outer function returns the inner function.

Here is the basic syntax:

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

Breaking this down:

  • outer is the enclosing function. It takes a parameter x.
  • inner is the nested function. It refers to x, which lives in the outer scope.
  • outer returns inner, not the result of calling inner.

Using the closure:

add_10 = outer(10)
print(add_10(5))   # 15
print(add_10(20))  # 30

Here, add_10 is a closure. It remembers x = 10 from the call to outer(10), even though outer has already finished.

Capturing Variables From the Outer Scope

The key idea is that the inner function does not copy the value. It keeps a reference to the variable in the outer scope.

def make_multiplier(factor):
    def multiply(value):
        return factor * value
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(8))  # 16
print(triple(8))  # 24

Each closure (double, triple) remembers a different factor.

Modifying Captured State With nonlocal

By default, you can read captured variables but not assign to them. To update a variable from the outer function, use nonlocal.

def make_counter():
    count = 0

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

    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3
  • count lives in the outer function.
  • nonlocal count tells Python that assignments to count should affect the outer count, not create a new local variable.

When to Use Closures in Python

Closures are handy any time you want to combine behavior with a bit of remembered state, without creating a full class. Here are some common situations.

1. Function Factories

Sometimes you want to generate functions with slightly different behavior. Closures let you “configure” a function once and then reuse it.

Examples:

  • Creating different discount calculators with fixed percentages.
  • Building validators with different rules (e.g., minimum length).
  • Generating SQL query builders with fixed table names.

2. Encapsulating State Without Classes

A closure can hold onto data that you do not want to expose directly. That gives you a simple form of encapsulation.

You might:

  • Keep an internal counter.
  • Track the last few values processed.
  • Store some configuration that should not be modified directly.

The caller gets a function to call, not a whole object with many attributes.

3. Callbacks and Event Handlers

In asynchronous code, GUI apps, or web frameworks, you often pass callbacks around. Closures let those callbacks remember context.

For example:

  • A button click handler that remembers which user is signed in.
  • A network callback that remembers the URL it fetched.
  • A retry function that remembers how many attempts are allowed.

Instead of passing extra parameters everywhere, you let the closure capture the data it needs.

4. Lightweight Decorators

Many decorators use closures behind the scenes. A decorator often returns a new function that wraps another function, and a closure captures the original function and any extra configuration.

For small, one-off decorators, a closure can feel simpler than building a whole class.


Examples of Python Closures

Let’s walk through several examples that mirror real use cases.

Example 1: Creating Simple Function Factories

Imagine a small app that applies different tax rates depending on where someone lives. You could write separate functions for each place, or generate them from a closure.

def make_tax_calculator(rate):
    def calculate(price):
        return price + price * rate
    return calculate

us_tax = make_tax_calculator(0.07)
eu_tax = make_tax_calculator(0.20)

print(us_tax(100))  # 107.0
print(eu_tax(100))  # 120.0

Here:

  • rate is captured by the closure.
  • us_tax and eu_tax are functions with different remembered rate values.

You avoid repeating the same logic and keep the code easy to read.

Example 2: A Counter With Hidden State

This pattern comes up often when you need a counter but do not want to expose a mutable global.

def make_id_generator(prefix: str):
    current = 0

    def next_id():
        nonlocal current
        current += 1
        return f"{prefix}-{current}"

    return next_id

ticket_id = make_id_generator("TICKET")
print(ticket_id())  # TICKET-1
print(ticket_id())  # TICKET-2
print(ticket_id())  # TICKET-3
  • current is hidden inside make_id_generator.
  • Only next_id() can change current.
  • The calling code has no direct access to modify current incorrectly.

You get a simple, controlled way to generate IDs, without designing a full class.

Example 3: Adding Context to Logging

Logging often needs context: which module, which user, which request. A closure can add that context without passing it into every call.

from datetime import datetime

def make_logger(prefix: str):
    def log(message: str) -> None:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] [{prefix}] {message}")
    return log

auth_log = make_logger("AUTH")
payment_log = make_logger("PAYMENT")

auth_log("Login successful for user alice@example.com")
payment_log("Processed order #5321")

Each closure remembers a different prefix, so the output stays easy to scan.

Example 4: Using Closures With Sorting

Python’s sorted() and list.sort() accept a key function. A closure can produce a key function that remembers how to sort.

def sort_by_field(field_name):
    def key_func(item):
        return item[field_name]
    return key_func

users = [
    {"username": "sam", "age": 35},
    {"username": "alex", "age": 29},
    {"username": "jordan", "age": 31},
]

by_age = sort_by_field("age")
by_username = sort_by_field("username")

print(sorted(users, key=by_age))
print(sorted(users, key=by_username))

The key_func closure captures field_name, so you can reuse the sorting logic with different fields.

Example 5: A Minimal Decorator Using a Closure

Closures also power decorators. Here’s a simple timing decorator:

import time
from functools import wraps

def timed(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        duration = time.perf_counter() - start
        print(f"{func.__name__} took {duration:.4f} seconds")
        return result
    return wrapper

@timed
def slow_add(x, y):
    time.sleep(0.5)
    return x + y

print(slow_add(3, 4))

Here:

  • wrapper is a closure that captures func.
  • start and duration are local to each call, but func stays bound to the original function.

Learn More About Python Closures

Once you understand the basic pattern, a few more details help you use closures effectively and avoid surprises.

Scope Rules and Free Variables

Closures rely on Python’s scope rules. A variable from the outer function that is used inside the inner function becomes a free variable in the closure.

Quick reminders:

  • Names created inside a function are local to that function.
  • Names from enclosing functions are available to nested functions.
  • Closures keep those names alive as long as the inner function exists.

You can inspect a function’s closure in code:

def make_adder(x):
    def add(y):
        return x + y
    return add

add_5 = make_adder(5)
print(add_5.__closure__)  # Tuple of cell objects holding captured variables

You rarely need to look at __closure__ in daily work, but it can help during debugging.

Late Binding and Loop Gotchas

One common pitfall with closures in Python is late binding. The inner function looks up variables when it runs, not when it’s defined.

Consider this example:

funcs = []

for i in range(3):
    def show():
        print(i)
    funcs.append(show)

for f in funcs:
    f()  # What do you expect?

The output is:

All closures share the same i, which ends as 2. To capture the current value at each iteration, use a default argument or another closure layer:

Using a default argument:

funcs = []

for i in range(3):
    def show(i=i):
        print(i)
    funcs.append(show)

for f in funcs:
    f()
# 0
# 1
# 2

Using a helper closure:

def make_show(n):
    def show():
        print(n)
    return show

funcs = [make_show(i) for i in range(3)]

for f in funcs:
    f()
# 0
# 1
# 2

Both techniques “freeze” the value at the time of creation.

Closures vs. Classes

Many examples that use closures could also be written using classes. For instance, the counter closure could become:

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count

counter = Counter()
print(counter.increment())
print(counter.increment())

How do you decide between them?

  • Use a closure when you need a small piece of behavior with a bit of hidden state and a simple interface.
  • Use a class when the concept has several operations, rich behavior, or needs inheritance.

Sometimes a closure is the quick, lightweight solution; sometimes a class keeps things clearer.

Performance and Memory Thoughts

A closure keeps references to captured variables. In most cases this cost is tiny. Still, a few habits help keep code healthy:

  • Avoid capturing large objects (like big dataframes or lists) unless you truly need them.
  • Avoid creating huge numbers of closures in tight loops if you can reuse a smaller number.
  • Remember that closures keep their environment alive; dropping references to closures lets Python garbage-collect that state.

You usually don’t need to micro-optimize closures, but awareness helps when performance matters.

How Closures Show Up in Real Code

Even if you never explicitly say “I’m writing a closure now,” you will use them in many idioms:

  • Sorting with a key function built on the fly.
  • Building URL routers where each route handler remembers its path pattern.
  • Writing decorators for caching, retry logic, or access control.
  • Creating small configuration helpers, like “loggers with prefixes” or “database query builders with a default schema.”

After a while, closures feel less like a special feature and more like a natural tool for capturing context.


Summary

A Python closure is a function that remembers values from its enclosing scope. You create one by defining a function inside another function, letting the inner function use variables from the outer function, and returning the inner function.

Learn to Code in Python for Free
Start learning now
button icon
To advance beyond this tutorial and learn Python by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.