functools.wraps on a functools.partial Copies the Wrong __doc__/__module__ (tested)
functools.wraps(f) copies __name__, __doc__, __module__ and more from f onto your wrapper, using getattr with a silent skip for whatever attributes f doesn't have. A plain function has all of these with meaningful values. A functools.partial object does not define its own __name__ or __qualname__ at all -- but it does inherit __doc__ and __module__ from the partial class itself, so wraps() copies those without skipping, silently attaching the generic functools.partial class docstring and module name to your wrapper instead of anything describing the actual call.
1. Decorating a partial gives you functools's own docstring, not yours
import functools
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
say_hi = functools.partial(greet, greeting="Hi")
def logged(f):
@functools.wraps(f)
def wrapper(*a, **kw):
return f(*a, **kw)
return wrapper
say_hi_logged = logged(say_hi)
print(say_hi_logged("Ann"))
print("__name__:", say_hi_logged.__name__)
print("__module__:", say_hi_logged.__module__)
print("__doc__:", say_hi_logged.__doc__)
Hi, Ann!
__name__: wrapper
__module__: functools
__doc__: Create a new function with partial application of the given arguments
and keywords.
Measured: __name__ stays "wrapper" -- unchanged, because partial objects have no __name__ attribute to copy and update_wrapper skips attributes that don't exist. But __module__ comes out as "functools" (the module where the partial class is defined) and __doc__ comes out as the generic class docstring for functools.partial itself -- not greet's docstring, not anything about say_hi. Both get copied silently because partial objects inherit those two attributes from their class, so the "attribute missing, skip it" fallback never triggers for them.
2. help() shows the misleading result directly
help(say_hi_logged)
Help on function wrapper in module functools:
wrapper(*args, sep=' ', end='\n', file=None, flush=False)
Create a new function with partial application of the given arguments
and keywords.
Measured: anyone running help() on the decorated callable sees "module functools" and a docstring about partial application in general -- actively misleading for debugging, since it looks like the function itself is part of the standard library rather than user code. (The signature line shown is a separate, correct effect of inspect.signature following the real __wrapped__ reference that wraps() also sets -- that part works as intended.)
3. The fix: set the metadata explicitly, or wrap the underlying function instead
import functools
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
say_hi = functools.partial(greet, greeting="Hi")
say_hi.__doc__ = "Greet someone with a Hi."
say_hi.__name__ = "say_hi"
say_hi.__module__ = greet.__module__
def logged(f):
@functools.wraps(f)
def wrapper(*a, **kw):
return f(*a, **kw)
return wrapper
say_hi_logged = logged(say_hi)
print("__name__:", say_hi_logged.__name__)
print("__module__:", say_hi_logged.__module__)
print("__doc__:", say_hi_logged.__doc__)
__name__: say_hi
__module__: __main__
__doc__: Greet someone with a Hi.
Measured: setting __name__/__doc__/__module__ directly on the partial object before decorating it (all three are plain writable attributes on a partial instance) gives wraps() real values to copy instead of falling through to the class-level defaults.
Frequently asked questions
Does functools.wraps set the wrong __doc__ when wrapping a functools.partial object?
Yes. We measured that wraps() on a partial object copies __doc__ and __module__ from the partial class itself (the generic 'Create a new function with partial application...' docstring and module "functools"), because partial instances inherit those two attributes from their class rather than lacking them.
Why doesn't __name__ get corrupted the same way?
Because functools.partial objects don't define __name__ or __qualname__ at all -- not even an inherited class-level one -- so update_wrapper's attribute copy silently skips them (it catches AttributeError per attribute), leaving the wrapper's own __name__ untouched. __doc__ and __module__ ARE present via class inheritance, so they get copied without being skipped.
How do I fix the misleading __doc__/__module__ after wrapping a partial?
Set __name__, __doc__, and __module__ directly on the partial object before passing it to a decorator that uses functools.wraps -- we measured this giving wraps() real attributes to copy instead of the generic class-level fallbacks.
Does this affect inspect.signature too?
No -- we measured inspect.signature(wrapper) correctly following the __wrapped__ reference back to the real partial and showing its actual call signature. Only __doc__/__module__ (and __name__ staying stale) are affected, not signature introspection.
Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in functools.wraps/update_wrapper: built a functools.partial over a user function, decorated it with a wrapper that applies functools.wraps(partial_obj), then inspected the resulting wrapper's __name__, __doc__, __module__, and help() output. PlainHeaders is our extension; Python's functools module is built in.