A two-level closure: outer takes the args and returns the actual decorator; inner takes the function and returns the wrapped function. functools.wraps copies metadata. The pattern shows up everywhere: @retry(max=3), @retry_on(Exception), @cached(ttl=60).
def retry(max=3): def deco(fn): @functools.wraps(fn); def wrap(*a, **kw): for i in range(max): try: return fn(*a, **kw); except Exception as e: if i==max-1: raise; return wrap; return deco
@retry(max=3) def flaky(): …
For class-based decorators, override call to behave like a closure.
@retry(max=3)
|
v
retry(3) returns deco
deco(flaky) returns wrapped
Two-level closure + @functools.wraps is the recipe. Memorize the skeleton.