#1 Data Analytics Program in India
₹2,499₹1,499Enroll Now
7 min read
•Question 25 of 41medium

Closures in Python

Understanding closures and scope.

What You'll Learn

  • What closures are and how they work
  • Python's LEGB scope resolution
  • The nonlocal keyword
  • Common closure patterns
  • Closures vs classes

What is a Closure?

A closure is a function that "remembers" variables from its enclosing scope, even after that scope has finished executing. It's created when a nested function references variables from its outer function.

code.pyPython
def outer(x):
    # x is in the enclosing scope

    def inner(y):
        return x + y  # x is "closed over"

    return inner  # Return the function object

# Create closures with different x values
add_5 = outer(5)
add_10 = outer(10)

print(add_5(3))   # 8  (5 + 3)
print(add_10(3))  # 13 (10 + 3)

# The functions "remember" their x values
print(add_5(7))   # 12 (5 + 7)

Python's LEGB Scope Rule

Python searches for variables in this order:

code.pyPython
# LEGB: Local -> Enclosing -> Global -> Built-in

x = "global"  # Global scope

def outer():
    x = "enclosing"  # Enclosing scope

    def inner():
        x = "local"  # Local scope
        print(x)     # "local"

    inner()
    print(x)  # "enclosing"

outer()
print(x)  # "global"

The nonlocal Keyword

To modify a variable in the enclosing scope (not just read it), use nonlocal:

code.pyPython
def counter():
    count = 0  # Enclosing variable

    def increment():
        nonlocal count  # Declare intent to modify
        count += 1
        return count

    return increment

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

# Without nonlocal, this would create a local variable
def broken_counter():
    count = 0

    def increment():
        count += 1  # UnboundLocalError!
        return count

    return increment

Practical Closure Patterns

Factory Functions

code.pyPython
def make_multiplier(n):
    """Create a function that multiplies by n."""
    def multiplier(x):
        return x * n
    return multiplier

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

print(double(5))  # 10
print(triple(5))  # 15

# Power functions
def make_power(exp):
    def power(base):
        return base ** exp
    return power

square = make_power(2)
cube = make_power(3)

Stateful Functions

code.pyPython
def running_average():
    """Maintain running average without a class."""
    total = 0
    count = 0

    def average(value):
        nonlocal total, count
        total += value
        count += 1
        return total / count

    return average

avg = running_average()
print(avg(10))  # 10.0
print(avg(20))  # 15.0
print(avg(30))  # 20.0

Configurable Functions

code.pyPython
def logger(prefix):
    """Create a logger with a fixed prefix."""
    def log(message):
        print(f"[{prefix}] {message}")
    return log

info = logger("INFO")
error = logger("ERROR")

info("Server started")    # [INFO] Server started
error("Connection failed")  # [ERROR] Connection failed

Memoization

code.pyPython
def memoize(func):
    """Cache function results using closure."""
    cache = {}

    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]

    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))  # Instant (cached)

Closures vs Classes

AspectClosureClass
SyntaxFunction-basedClass-based
StateIn enclosing scopeIn instance attributes
MethodsUsually oneMultiple methods
VisibilityTruly privateConvention (_var)
Use CaseSimple stateful functionsComplex objects
code.pyPython
# Closure approach
def make_account(initial_balance):
    balance = initial_balance

    def transact(amount):
        nonlocal balance
        balance += amount
        return balance

    return transact

# Class approach
class Account:
    def __init__(self, initial_balance):
        self.balance = initial_balance

    def transact(self, amount):
        self.balance += amount
        return self.balance

# Usage is similar
closure_account = make_account(100)
class_account = Account(100)

closure_account(-20)  # 80
class_account.transact(-20)  # 80

Common Pitfall: Late Binding

code.pyPython
# Problem: All functions share the same i!
funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2] - Not [0, 1, 2]!

# Solution: Capture value with default argument
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)

print([f() for f in funcs])  # [0, 1, 2]

Interview Tip

When asked about closures:

  1. A closure captures variables from enclosing scope
  2. LEGB: Local → Enclosing → Global → Built-in
  3. Use nonlocal to modify enclosing variables
  4. Common for factories, decorators, callbacks
  5. Watch for late binding in loops