update_wrapper and __wrapped__

functools · Expert · 11 min

Framework authors and decorator libraries need precise control over what tools see.

wraps(func) is partial(update_wrapper, wrapped=func). update_wrapper(wrapper, wrapped):

1. Copies each attribute in WRAPPER_ASSIGNMENTS (__module__, __name__, __qualname__, __doc__, __annotations__, __type_params__) if present on wrapped. 2. Updates attributes in WRAPPER_UPDATES (__dict__) by merging, so custom attributes set on the original survive. 3. Sets wrapper.__wrapped__ = wrapped, after the copying, so it isn't overwritten by a copied __wrapped__.

You can pass custom assigned/updated tuples, for example to skip copying __annotations__ when the wrapper changes types.

__wrapped__ forms a chain through stacked decorators. inspect.unwrap(f) follows it to the innermost function; inspect.signature follows it too, unless the wrapper defines __signature__. That's the hook for signature-changing decorators: if your decorator injects or removes a parameter (dependency injection, a retries= option), set wrapper.__signature__ to an adjusted inspect.Signature so introspection matches what callers may actually pass.

Class-based decorators should call update_wrapper(self, func) in __init__, and implement __get__ if they must work on methods, since instances of ordinary classes don't bind self like functions do.

Example

import inspect
from functools import wraps

def with_retries(func):
    @wraps(func)
    def wrapper(*args, retries=3, **kwargs):
        for attempt in range(retries):
            try:
                return func(*args, **kwargs)
            except ConnectionError:
                if attempt == retries - 1:
                    raise
    sig = inspect.signature(func)
    extra = inspect.Parameter("retries", inspect.Parameter.KEYWORD_ONLY, default=3)
    wrapper.__signature__ = sig.replace(parameters=[*sig.parameters.values(), extra])
    return wrapper

@with_retries
def fetch(url: str) -> str:
    return f"GET {url}"

print(inspect.signature(fetch))          # (url: str, *, retries=3) -> str
print(inspect.unwrap(fetch) is fetch.__wrapped__)   # True
print(fetch("x", retries=1))             # GET x

Remember

  • wraps = update_wrapper: copy metadata, merge __dict__, set __wrapped__
  • inspect.unwrap and inspect.signature follow __wrapped__ chains
  • Set __signature__ when a decorator changes the accepted parameters

Watch out for

  • Writing a class-based decorator without __get__ and finding it breaks when applied to methods.

Check yourself

  1. A decorator adds a keyword-only timeout parameter. How can it make inspect.signature report that parameter?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.update_wrapper