Python Nested Function: Scope, Closures, and Use Cases
Understand how python nested function scope works, how closures capture variables, and when inner functions improve code structure.
python nested function requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Python, a nested function is a function defined inside another function's body. The inner function is only created when the outer function runs, and it can reference variables from the enclosing scope.
The Basic Syntax of a Nested Function
A nested function is declared with the same def statement used for any other function, but it appears inside the body of another function.
def outer(): message = "hello" def inner(): print(message) inner() outer()
The inner function inner can read message from the enclosing scope. This is the core mechanism that makes nested functions useful: the inner function has access to the outer function's local variables without them being passed as arguments.
How Scope and Name Resolution Work
When Python executes a nested function, it resolves names in a specific order: local scope, enclosing scopes, global scope, builtins. The inner function can read variables from the enclosing scope, but assignment behaves differently.
def counter(): count = 0 def increment(): count += 1 return count return increment
This raises UnboundLocalError because count += 1 is an assignment, which makes count local to increment. To modify a variable from the enclosing scope, use nonlocal:
def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment
The nonlocal keyword tells Python that count belongs to the nearest enclosing scope rather than the local scope of increment.
Closures: When the Inner Function Outlives the Outer Call
When a nested function is returned from the outer function, it retains access to the enclosing scope even after the outer function has finished executing. This is a closure.
def make_multiplier(factor): def multiply(value): return value * factor return multiply double = make_multiplier(2) print(double(5)) # 10
Each call to make_multiplier creates a new closure with its own factor. The variables are captured by reference, which matters when the outer function changes a variable after the closure is created.
def make_callbacks(): callbacks = [] for i in range(3): def callback(): return i callbacks.append(callback) return callbacks for cb in make_callbacks(): print(cb()) # 2, 2, 2
All three closures capture the same i variable, which has reached its final value of 2 by the time the callbacks execute. To capture the current value at creation time, bind it as a default argument:
def make_callbacks(): callbacks = [] for i in range(3): def callback(i=i): return i callbacks.append(callback) return callbacks for cb in make_callbacks(): print(cb()) # 0, 1, 2
Practical Patterns: Validation and Setup
Nested functions are useful when you need to encapsulate helper logic that only makes sense within one function. A common pattern is validating arguments before processing:
def process_order(order, inventory): def validate(item): return item in inventory and inventory[item] > 0 if not all(validate(item) for item in order): raise ValueError("One or more items are unavailable") # process order...
The validate helper is only meaningful inside process_order, so defining it locally keeps the module namespace clean and signals that the helper is an implementation detail.
Function Factories and Decorators
Nested functions are the building block for decorators. A decorator is a function that takes a function and returns a wrapped version:
def log_calls(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @log_calls def add(a, b): return a + b
The wrapper function is a nested function that closes over func. Every decorated function gets its own wrapper with its own captured func, which is why decorators work correctly when applied to multiple functions.
Performance and Runtime Cost
Creating a nested function has a small cost: Python must create a new function object each time the outer function runs. For most code this cost is negligible. However, if the outer function runs in a tight loop and creates a nested function on every iteration, the allocation overhead can add up.
More significant is the closure cell overhead. Variables captured by a closure are stored in cell objects rather than plain local slots, which makes access slightly slower than a direct local variable. In practice, this rarely matters unless the inner function is called millions of times.
The bigger performance concern is misuse: using a nested function where a module-level helper would be clearer. The runtime cost is similar, but the maintenance cost is higher because the nested function cannot be tested or reused independently.
Common Mistakes and Edge Cases
One frequent mistake is expecting the nested function to be accessible outside the outer function. It is not; the name only exists during the outer call.
Another is confusing nonlocal with global. nonlocal refers to the nearest enclosing scope, not the module scope. If no enclosing scope defines the variable, Python raises SyntaxError.
Late binding is the most subtle issue. As shown earlier, closures capture variables, not values. If the outer function mutates a captured variable after creating the closure, the closure sees the new value.
When a Nested Function Is Not the Right Choice
If a helper is used by more than one function, it should be a module-level function. If the helper needs to be tested in isolation, module level is also better. Nested functions cannot be imported or unit-tested directly.
If the logic is simple enough for a lambda, a lambda may be clearer:
sorted(items, key=lambda item: item["priority"])
But a lambda cannot contain statements, so a nested function is required when the logic needs multiple statements or local variables.
Compatibility Considerations
Nested functions and closures work in all supported Python versions. The nonlocal keyword was introduced in Python 3; Python 2 used a workaround with mutable containers. Since Python 2 is no longer supported, this is rarely a concern, but codebases with legacy Python 2 code may still contain the workaround pattern.