Summary
As implemented in CPython, @functools.wraps updates its argument (the wrapper) and returns the same object back. Since it is explicitly described as a convenience wrapper around functools.update_wrapper() in the docs, I would suppose this update-not-replace behavior to be part of the standard.
However, as it is currently implemented in typeshed, the return value of functools.update_wrapper() is typed only on the signatures of the wrapper and wrapped callables (_Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]), without consideration of the type of wrapper. This results in permissible operations on the return value being marked illegal by type-checkers like mypy.
Example
(mypy playground: https://mypy-play.net/?mypy=latest&python=3.13&gist=26b45882b3db03d4b4c2ec74db78cd02)
Here is an example passed to mypy 2.1.0:
from collections.abc import Callable
from functools import wraps
from inspect import signature
from types import FunctionType
def func() -> None:
pass
def wrap_vanilla[**PS, T](func: Callable[PS, T]) -> Callable[PS, T]:
@wraps(func)
def wrapper(*a: PS.args, **k: PS.kwargs) -> T:
return func(*a, **k)
wrapper.__signature__ = signature(func)
return wrapper
def wrap_with_extra_nudge[**PS, T](func: Callable[PS, T]) -> Callable[PS, T]:
@wraps(func)
def wrapper(*a: PS.args, **k: PS.kwargs) -> T:
return func(*a, **k)
# Because `typeshed` represents the return value of `@wraps` with
# `_Wrapper[PSWrapped, TWrapped, PSWrapper, TWrapper]`, it is not recognized
# as a plain function without this assertion, despite the implementation of
# `functools.update_wrapper()` guaranteeing that the `wrapper` object itself
# is returned
assert isinstance(wrapper, FunctionType)
wrapper.__signature__ = signature(func)
return wrapper
Expected behavior
Both wrap_vanilla() and wrap_with_extra_nudge() pass type-checking.
Actual behavior
wrap_vanilla() fails type-checking because wraps(func)(wrapper) is not recognized as being of the same type as, and indeed being identical to, wrapper (a FunctionType):
main.py:16: error: "_Wrapped[PS, T, PS, T]" has no attribute "__signature__" [attr-defined]
Found 1 error in 1 file (checked 1 source file)
Possible related issues
#4626, #9846
Summary
As implemented in CPython,
@functools.wrapsupdates its argument (thewrapper) and returns the same object back. Since it is explicitly described as a convenience wrapper aroundfunctools.update_wrapper()in the docs, I would suppose this update-not-replace behavior to be part of the standard.However, as it is currently implemented in
typeshed, the return value offunctools.update_wrapper()is typed only on the signatures of thewrapperandwrappedcallables (_Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]), without consideration of the type ofwrapper. This results in permissible operations on the return value being marked illegal by type-checkers likemypy.Example
(
mypyplayground: https://mypy-play.net/?mypy=latest&python=3.13&gist=26b45882b3db03d4b4c2ec74db78cd02)Here is an example passed to
mypy 2.1.0:Expected behavior
Both
wrap_vanilla()andwrap_with_extra_nudge()pass type-checking.Actual behavior
wrap_vanilla()fails type-checking becausewraps(func)(wrapper)is not recognized as being of the same type as, and indeed being identical to,wrapper(aFunctionType):Possible related issues
#4626, #9846