Python Inner Function: Syntax, Scope, and Use Cases
Understand python inner function syntax, closure behavior, and practical uses like decorators and factory functions.
What a Python Inner Function Is
A python inner function is a function defined inside another function's body. It is created fresh each time the outer function executes, and it can read variables from the enclosing scope.
def outer(): def inner(): return "hello from inner" return inner()
The name inner is local to outer. Calling inner() at module level raises a NameError because the name is not visible outside the enclosing function. This scoping behavior is the first thing to understand about nested functions.
How Scope Works for Nested Functions
Python resolves names using the LEGB rule: Local, Enclosing, Global, Built-in. An inner function can read any variable from the enclosing function's scope without extra syntax.
def outer(value): def inner(): return value * 2 return inner()
Here inner reads value from outer's local scope. The lookup happens at runtime, so if outer changes value before calling inner, the inner function sees the updated value.
Closures: Capturing State
When the outer function returns the inner function object instead of calling it, the inner function keeps a reference to the enclosing scope. This combination of a function and its captured scope is a closure.
def multiplier(factor): def multiply(x): return x * factor return multiply double = multiplier(2) triple = multiplier(3) print(double(5)) # 10 print(triple(5)) # 15
Each call to multiplier creates a separate closure with its own captured factor. The closure survives after multiplier has returned because the inner function holds a reference to the enclosing frame's variables.
Using nonlocal to Modify Enclosing Variables
Reading an enclosing variable works automatically. Reassigning it does not. Without nonlocal, an assignment inside the inner function creates a new local variable and raises UnboundLocalError when you try to read the original.
def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment next_count = counter() print(next_count()) # 1 print(next_count()) # 2
The nonlocal statement tells Python that count refers to the variable in the nearest enclosing scope. This is the mechanism behind stateful closures like counters and accumulators.
Inner Functions for Encapsulation
Inner functions keep helper logic private to the outer function. The helpers cannot be imported, tested in isolation, or accidentally called from other modules.
def process_data(data): def validate(item): return item is not None and isinstance(item, str) def clean(item): return item.strip().lower() return [clean(i) for i in data if validate(i)]
This pattern is useful when the helpers are only meaningful within the context of process_data. If the helpers are needed elsewhere, they should be module-level functions instead.
Inner Functions as Factory Functions
A factory function returns a configured inner function. This lets you create specialized callables without repeating configuration logic.
def make_formatter(prefix, suffix): def format_text(text): return f"{prefix}{text}{suffix}" return format_text bracket = make_formatter("[", "]") print(bracket("value")) # [value]
The factory pattern is common in configuration-driven code where the same transformation logic applies with different parameters.
Decorators Rely on Inner Functions
A decorator is a function that receives a function and returns a wrapped version. The wrapper is typically an inner function that preserves the original call signature.
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 inner function captures func from the enclosing scope. Every call to add goes through the wrapper, which logs the call and delegates to the original function.
Performance and Maintainability Considerations
Creating an inner function allocates a new function object each time the outer function runs. In a loop that executes millions of times, this allocation cost is measurable. If the inner function does not depend on the enclosing scope, moving it to module level avoids repeated allocation.
Maintainability is the main reason to use inner functions. They keep related logic close together and prevent helper functions from leaking into the module namespace. When a helper grows complex or needs its own tests, promote it to a module-level function.
Common Mistakes with Inner Functions
The most frequent mistake is forgetting nonlocal when reassigning an enclosing variable. Another is expecting the inner function to be accessible outside the outer function. A third is capturing a loop variable in a closure.
funcs = [] for i in range(3): def f(): return i funcs.append(f) print([fn() for fn in funcs]) # [2, 2, 2]
All three closures capture the same i variable, which ends at 2 after the loop. Use a default argument or a factory function to capture each value:
funcs = [] for i in range(3): def f(i=i): return i funcs.append(f) print([fn() for fn in funcs]) # [0, 1, 2]
The default argument binds the current value of i at definition time, giving each closure its own copy.