Python - Python Closures and Nested Functions

Introduction

In Python, a nested function is a function defined inside another function. Nested functions are useful when a particular function is needed only within another function and should not be directly accessible from outside.

A closure is created when a nested function remembers and continues to access variables from its enclosing function even after the enclosing function has finished executing. Closures are an important concept in Python because they allow functions to preserve data between calls without using global variables or classes.

Understanding nested functions and closures helps in working with callbacks, function factories, decorators, data hiding, and customized behavior.

1. What Is a Nested Function?

A nested function is simply a function defined inside another function.

def outer_function():
    def inner_function():
        print("This is the inner function")

    inner_function()

outer_function()

Output:

This is the inner function

Here, outer_function() is the outer function, while inner_function() is the nested function.

The inner_function() exists within the local scope of outer_function(). It can normally be called directly only from inside the outer function.

2. Why Use Nested Functions?

Nested functions are useful when a function performs a small task that is relevant only to another function.

For example:

def calculate_total():
    def add_tax(amount):
        return amount * 1.18

    price = 1000
    total = add_tax(price)

    return total

print(calculate_total())

Output:

1180.0

The add_tax() function is not needed elsewhere in the program, so keeping it inside calculate_total() makes the code more organized and limits its scope.

Nested functions can also help divide a complicated operation into smaller logical parts.

3. What Is a Closure?

A closure occurs when a nested function remembers variables from its enclosing function.

Consider the following example:

def outer_function(message):
    def inner_function():
        print(message)

    return inner_function

my_function = outer_function("Hello Python")
my_function()

Output:

Hello Python

The important part is that outer_function() has already completed execution when my_function() is called.

Normally, you might expect the local variable message to disappear when outer_function() finishes. However, the returned inner_function() retains access to message.

This retained reference is what makes inner_function() a closure.

4. How a Closure Works

A closure generally involves three components:

  1. An outer function.

  2. A nested function.

  3. A variable from the outer function that the nested function uses.

Example:

def create_message(name):
    def message():
        return "Hello " + name

    return message

greeting = create_message("Rahul")

print(greeting())

Output:

Hello Rahul

The variable name belongs to create_message(), but the nested message() function continues to remember it.

Conceptually, the returned function carries access to the environment in which it was created.

5. Closures Preserve Values

One useful feature of closures is that they can preserve different values for different function instances.

def create_multiplier(number):
    def multiply(value):
        return value * number

    return multiply

double = create_multiplier(2)
triple = create_multiplier(3)

print(double(10))
print(triple(10))

Output:

20
30

Here, double remembers number = 2, while triple remembers number = 3.

Both functions were created using the same outer function, but they maintain different enclosed values.

6. Closure with Multiple Variables

A closure can remember more than one variable from its enclosing scope.

def create_profile(name, age):
    def display():
        print("Name:", name)
        print("Age:", age)

    return display

profile = create_profile("Anita", 25)
profile()

Output:

Name: Anita
Age: 25

The nested function remembers both name and age.

7. Closures and the nonlocal Keyword

A nested function can read a variable from its enclosing function. However, if it needs to modify that variable, Python provides the nonlocal keyword.

Example:

def counter():
    count = 0

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

    return increment

counter_function = counter()

print(counter_function())
print(counter_function())
print(counter_function())

Output:

1
2
3

Here, count belongs to the enclosing counter() function.

The nonlocal keyword tells Python that count should be modified in the enclosing scope rather than creating a new local variable.

Without nonlocal, the assignment count += 1 would cause a local-variable scope problem.

8. Closures vs Global Variables

Closures can provide a controlled alternative to global variables.

Using a global variable:

count = 0

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

This works, but the variable is available at the global level.

Using a closure:

def create_counter():
    count = 0

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

    return increment

The count variable is hidden inside the enclosing function. External code can interact with it through the returned function without directly accessing the variable.

This can make programs easier to organize and reduce unwanted modification of shared data.

9. Closures for Data Hiding

Closures can be used to keep certain data private.

def bank_account(initial_balance):
    balance = initial_balance

    def get_balance():
        return balance

    def deposit(amount):
        nonlocal balance
        balance += amount

    return get_balance, deposit

get_balance, deposit = bank_account(1000)

print(get_balance())

deposit(500)

print(get_balance())

Output:

1000
1500

The balance variable cannot be directly accessed through the returned functions. It is maintained inside the enclosing scope.

This provides a simple form of data hiding.

10. Function Factories Using Closures

A function factory is a function that creates and returns another function.

Closures are commonly used for this purpose.

def create_power_function(power):
    def calculate(number):
        return number ** power

    return calculate

square = create_power_function(2)
cube = create_power_function(3)

print(square(4))
print(cube(4))

Output:

16
64

create_power_function() creates customized functions based on the value passed to it.

This technique is useful when the same basic operation needs to be configured differently.

11. Closures and Callbacks

Closures can also be useful when a function needs to remember configuration information while being passed around as another function.

For example:

def create_greeting(prefix):
    def greet(name):
        return prefix + ", " + name

    return greet

formal_greeting = create_greeting("Good morning")
casual_greeting = create_greeting("Hello")

print(formal_greeting("Anita"))
print(casual_greeting("Rahul"))

Output:

Good morning, Anita
Hello, Rahul

The functions can be stored in variables and passed to other functions just like any other Python function.

12. Checking Whether a Function Is a Closure

Python functions have an attribute called __closure__.

Example:

def outer():
    message = "Hello"

    def inner():
        print(message)

    return inner

function = outer()

print(function.__closure__)

If the function contains enclosed variables, __closure__ can contain references to those variables.

The enclosed values can be accessed through closure cells:

def outer():
    message = "Hello Python"

    def inner():
        print(message)

    return inner

function = outer()

print(function.__closure__[0].cell_contents)

Output:

Hello Python

This is mainly useful for understanding and inspecting closures rather than for normal application programming.

13. Important Difference Between Nested Functions and Closures

A nested function and a closure are related, but they are not exactly the same thing.

A nested function is simply a function defined inside another function:

def outer():
    def inner():
        print("Hello")

    inner()

A closure occurs when the nested function uses and retains access to variables from its enclosing scope:

def outer(message):
    def inner():
        print(message)

    return inner

Therefore, every closure involves a nested function, but a nested function does not necessarily form a closure.

14. Common Applications of Closures

Closures are useful in several programming situations.

Function factories: Creating customized functions based on configuration values.

Data hiding: Keeping variables inside an enclosing function and exposing controlled operations.

Callbacks: Creating functions that remember specific configuration or state.

Decorators: Closures are commonly used as part of the implementation of function decorators.

State management: Maintaining information between multiple calls without relying on global variables.

Configuration: Creating functions that retain settings such as thresholds, prefixes, multipliers, or other parameters.

15. Advantages of Closures

Closures provide several benefits.

Encapsulation: Data can be kept inside an enclosing scope rather than being exposed globally.

State preservation: Values can remain available between function calls.

Reduced global state: Closures can avoid unnecessary global variables.

Reusable function creation: A single outer function can generate many customized functions.

Logical organization: Related data and behavior can be kept together.

16. Limitations of Closures

Closures also have some limitations.

They can be difficult for beginners to understand because variables may remain available even after the outer function has completed.

Excessive use of nested functions can make code harder to read and maintain.

For complex state management, a class may sometimes provide a clearer structure than a closure.

Closures can also cause unexpected behavior when variables are captured inside loops, particularly when the functions are executed later.

17. Closure and Loop Example

Consider this example:

functions = []

for i in range(3):
    def show():
        print(i)

    functions.append(show)

for function in functions:
    function()

The output is:

2
2
2

This can be surprising.

The functions do not independently store the value of i at each iteration. They refer to the same variable, whose final value is 2.

One way to solve this is to capture the current value using a default argument:

functions = []

for i in range(3):
    def show(value=i):
        print(value)

    functions.append(show)

for function in functions:
    function()

Output:

0
1
2

This is an important practical consideration when working with closures.

18. Nested Functions and Scope

Nested functions are closely related to Python's scope rules.

Python searches for variables using the LEGB rule:

  • Local

  • Enclosing

  • Global

  • Built-in

For a nested function, the enclosing scope is particularly important because it allows the inner function to access variables defined in the outer function.

Example:

def outer():
    message = "Outer value"

    def inner():
        print(message)

    inner()

outer()

The inner() function first looks for message in its local scope. Since it does not find it there, Python searches the enclosing scope and finds the variable defined in outer().

Conclusion

Nested functions allow Python programmers to define functions inside other functions, keeping helper functionality within an appropriate scope. Closures extend this idea by allowing a nested function to retain access to variables from its enclosing function even after the outer function has finished executing.

Closures are particularly useful for creating function factories, maintaining state, hiding data, handling callbacks, and building reusable program components. The nonlocal keyword is important when a closure needs to modify a variable belonging to its enclosing scope. Understanding how nested functions, enclosing scopes, and closures work provides a strong foundation for more advanced Python concepts such as decorators and functional programming.