Attaching Nonlocal Variables to Nested Functions in Python 2026

Writing Functions Mar 16, 2026

Attaching Nonlocal Variables to Nested Functions in Python 2026

Attaching nonlocal variables to nested functions (creating closures) is a powerful technique in Python. It allows inner functions to "remember" and modify variables from their enclosing scope even after the outer function has finished executing.

TL;DR — Key Takeaways 2026

  • Use nonlocal to modify variables from the enclosing scope
  • The inner function "closes over" the nonlocal variables, creating a closure
  • This pattern is widely used for factory functions, decorators, and stateful callbacks
  • Keep closures simple — for complex state, prefer classes

1. Basic Closure with nonlocal


def make_counter():
    count = 0
    
    def counter():
        nonlocal count
        count += 1
        return count
    
    return counter

# Each call creates its own independent closure
c1 = make_counter()
c2 = make_counter()

print(c1())   # 1
print(c1())   # 2
print(c2())   # 1
print(c1())   # 3

2. Advanced Example – Configurable Factory


def make_formatter(prefix: str, suffix: str = ""):
    """Returns a customized formatting function."""
    
    def formatter(text: str) -> str:
        nonlocal prefix, suffix          # Allows modification if needed
        return f"{prefix}{text}{suffix}"
    
    return formatter

# Create specialized formatters
error_msg = make_formatter("[ERROR] ", "!")
warning_msg = make_formatter("[WARNING] ")

print(error_msg("File not found"))      # [ERROR] File not found!
print(warning_msg("Low disk space"))    # [WARNING] Low disk space

3. Best Practices in 2026

  • Use nonlocal explicitly when modifying enclosing variables
  • Keep the outer function focused and the inner function small
  • Document what variables the closure depends on
  • Use type hints with Callable for the returned function
  • When state becomes complex, switch to a class instead of a closure

Where Closure Variables Live

When an inner function uses a variable from its enclosing function, Python stores that variable in a cell. The inner function keeps a reference to the cell in its __closure__ attribute, and the variable names are listed in __code__.co_freevars. This is how a returned function still sees variables after the outer function has finished.

Each call of the outer function creates new cells, so every returned function has its own state. With nonlocal, the inner function can update that state between calls.

Inspecting cells is mainly useful for learning and debugging, but it makes clear that closures hold variables, not copies of values.

Because a closure keeps its cells alive, large objects captured by a closure stay in memory as long as the function exists.

Worked Example: Two Accumulators With Separate Cells

We create two accumulators and inspect their closure cells.

def make_accumulator(start=0):
    total = start
    def add(amount):
        nonlocal total
        total += amount
        return total
    return add

sales = make_accumulator()
costs = make_accumulator(100)
sales(50); sales(25); costs(10)

print(sales.__code__.co_freevars)
print(sales.__closure__[0].cell_contents, costs.__closure__[0].cell_contents)
print(sales.__closure__[0] is costs.__closure__[0])

Both functions have one free variable named total. The sales cell holds 75 and the costs cell holds 110, and the cells are different objects, so the two accumulators never interfere.

Practical Tips

Use closures for small amounts of state tied to one behaviour, such as a running total, a counter or a cache. For several related values or methods, a class is clearer.

When debugging a closure, printing f.__code__.co_freevars together with the cell contents shows exactly which values the function remembers. This is a quick way to spot a closure that captured the wrong variable.

Avoid capturing large objects in closures that live for a long time, such as callbacks registered with a framework.

Give factory functions names that start with make_ or build_ so readers know they return a configured function.

Closures created inside loops share the loop variable unless you bind each value with a default argument or functools.partial. Check this whenever you build several closures at once.

For simple counters, itertools.count is a ready-made alternative.

Common Mistakes

  • Thinking a closure copies the value at creation time.
  • Capturing large DataFrames in long-lived closures.
  • Expecting two factory results to share state.
  • Relying on __closure__ in production code.

Quick Summary

  • Closure variables live in cells.
  • __closure__ and co_freevars show them.
  • Each factory call creates new cells.
  • Captured objects stay alive with the function.

Conclusion

Attaching nonlocal variables to nested functions is a clean and efficient way to create stateful, reusable functions in Python. In 2026, this closure pattern remains one of the most elegant ways to implement factory functions, decorators, and lightweight state management.

Next steps:

  • Identify repeated logic in your code and replace it with closure-based factories

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices