Gyaan

Closures and Nonlocal

intermediate closures nonlocal scope first-class-functions

A closure is a function that remembers the variables from its enclosing scope, even after that scope has finished executing. To understand closures, we first need to know that functions in Python are first-class objects — we can pass them around, return them from other functions, and assign them to variables.

First-Class Functions

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

# Assign a function to a variable
say_hello = greet
say_hello("Manish")  # "Hello, Manish!"

# Pass a function as an argument
def call_twice(func, arg):
    return func(arg) + " " + func(arg)

call_twice(greet, "World")  # "Hello, World! Hello, World!"

Inner Functions

We can define functions inside other functions. The inner function has access to the outer function’s variables.

def outer():
    message = "Hello from outer"

    def inner():
        print(message)  # can access outer's variable

    inner()

outer()  # "Hello from outer"

What Makes It a Closure?

A closure happens when we return the inner function, and it keeps a reference to the enclosing variables even after the outer function has finished running.

Closure Scope Chain
Global Scope
outer(multiplier=3)
multiplier = 3 ← captured by closure
inner(x)
return x * multiplier
↑ Looks up multiplier from enclosing scope
return inner ← inner remembers multiplier=3
def make_multiplier(multiplier):
    def multiply(x):
        return x * multiplier  # remembers multiplier from outer scope
    return multiply  # return the function, don't call it

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

double(5)   # 10 — multiplier=2 is remembered
triple(5)   # 15 — multiplier=3 is remembered

Even though make_multiplier has finished executing, the returned multiply function still has access to the multiplier variable. That’s a closure.

The nonlocal Keyword

By default, an inner function can read variables from the enclosing scope but can’t reassign them. If we try, Python creates a new local variable instead. The nonlocal keyword lets us modify the enclosing variable.

def counter():
    count = 0

    def increment():
        nonlocal count     # without this, we'd get an UnboundLocalError
        count += 1
        return count

    return increment

tick = counter()
tick()  # 1
tick()  # 2
tick()  # 3 — count persists between calls

Without nonlocal, Python would think count += 1 is trying to read a local variable count before it’s been assigned.

Practical Uses

Factory Functions

Closures are great for creating specialized functions.

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

casual = make_greeter("Hey")
formal = make_greeter("Good evening")

casual("Manish")   # "Hey, Manish!"
formal("Manish")   # "Good evening, Manish!"

Data Hiding

Closures can act like lightweight objects, encapsulating state without a class.

def bank_account(initial_balance):
    balance = initial_balance

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

    return transact

account = bank_account(100)
account(50)    # 150 (deposit)
account(-30)   # 120 (withdrawal)

In simple language, a closure is an inner function that carries a little backpack of variables from its parent function. Even after the parent is gone, the backpack stays.