2025, Oct 04 03:00
Prevent nonlocal state resets in composed Python decorators: wrap once at decoration time, not at call time
Learn why composing Python decorators at call time resets nonlocal state, and how wrapping once at decoration time preserves state and fixes first-call logic.
When composing decorators in Python, it’s easy to accidentally reset state if you wrap a function at call time instead of decoration time. A typical symptom is a nonlocal flag in a parent decorator that never flips the way you expect when a child decorator delegates to it on every call.
Reproducing the issue
Below is a minimal example where a child decorator relies on a parent decorator that keeps a nonlocal toggle. The goal is to run different logic on the first call vs subsequent calls, but the behavior never progresses to the “after” branch.
def base_decorator(fn):
    primed = False
    def inner(*a, **kw):
        nonlocal primed
        if primed:
            print("executing after first call")
            return fn(*a, **kw)
        else:
            print("executing before first call")
            primed = True
            out = fn(*a, **kw)
            return out
    return inner
def should_allow():
    return True
def child_decorator(fn):
    def inner(*a, **kw):
        if should_allow():
            composed = base_decorator(fn)
            return composed(*a, **kw)
        else:
            print("do nothing")
    return inner
class Runner:
    def __init__(self):
        print("init called")
    @child_decorator
    def run(self):
        print("Hello from run")
obj = Runner()
obj.run()
obj.run()What’s going on
The nonlocal variable lives inside the closure created by the parent decorator. In the code above, the child decorator calls the parent decorator inside the wrapped function body. That means a new closure is constructed on every invocation. Each call starts with a fresh nonlocal state, so the check keeps hitting the “before” branch and never advances to the “after” branch.
The fix
Wrap once at decoration time, not at call time. Move the call to the parent decorator outside the inner function, so the closure (and its nonlocal flag) is created a single time and preserved across invocations.
def child_decorator(fn):
    composed = base_decorator(fn)
    def inner(*a, **kw):
        if should_allow():
            return composed(*a, **kw)
        else:
            print("do nothing")
    return innerWith this change, the first call uses the “before” branch and flips the flag, while later calls land in the “after” branch, exactly as intended. The parent decorator remains untouched.
Why this matters
Decorator behavior depends on when wrapping occurs. If you re-wrap inside the call path, you recreate all associated closure state, including nonlocal variables that track first-run or memoized state. For decorators that coordinate initialization, gating, or one-time setup, this distinction is critical to avoid subtle bugs and duplicated work.
Takeaways
When composing decorators, ensure that wrapping happens once—at decoration time—so closures hold stable state through the lifetime of the function. If a parent decorator maintains nonlocal state, keep the child decorator’s integration outside the hot path and pass calls straight to the already-wrapped function. This pattern keeps behavior predictable and avoids surprising resets.
The article is based on a question from StackOverflow by Rohit Pathak and an answer by Abdul Aziz Barkat.