You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
functools.update_wrapper breaks the signature of functools.partial objects #90917
It's considered good hygiene to use functools.update_wrapper() to make your wrapped functions look like the original. However, when using functools.partial() to pre-supply arguments to a function, if you then call functools.update_wrapper() to update that partial object, inspect.signature() returns the *original* function's signature, not the *wrapped* function's signature.
To be precise: if you wrap a function with functools.partial() to pre-provide arguments, then immediately call inspect.signature() on that partial object, it returns the correct signature with the pre-filled parameters removed. If you then call functools.update_wrapper() to update the partial from the original function, inspect.signature() now returns the *wrong* signature.
I looked into it a little. The specific thing changing inspect.signature()'s behavior is the '__wrapped__' attribute added by functools.update_wrapper(). By default inspect.signature() will unwrap partial objects, but only if it has a '__wrapped__' attribute.
This all looks pretty deliberate. And it seems like there was some thought given to this wrinkle; inspect.signature() takes a "follow_wrapper_chains" parameter the user can supply to control this behavior. But the default is True, meaning that by default it unwraps partial objects if they have a '__wrapped__'.
I admit I don't have any context for this. Why do we want inspect.signature() to return the wrong signature by default?
You make a good point. I filed a separate bug (bpo-46846) suggesting that partial objects should set their own annotations and signature. I agree that objects performing such magic should take care of these details themselves, rather than requiring the inspect module to have workarounds based on deep knowledge of these other modules' inner workings.
I heard back from both Samuel Colvin (Pydantic) and Sebastián Ramírez (FastAPI). They said neither of them use update_wrapper with partial objects. They also took a peek in a bunch of other projects (FastAPI, Typer, SQLModel, Asyncer, SQLAlchemy, Trio, and AnyIO) and nobody was doing it.
So honestly it seems like nobody (but me!) calls update_wrapper on partial objects, and we can just fix it.
Graham, any final thoughts before we start pulling levers and merging PRs? For now I just want to fix this bug. I'm in favor of re-engineering the relevant objects so they write their own __signature__ objects, so inspect.Signature doesn't have to understand the internals of objects from other modules. But maybe we save that for another day.
grahamd commented on Mar 25, 2022
grahamdmannequin
Mannequin
More actions
It is Django I would worry about and look at closely as they do stuff with decorators on instance methods that uses partials.
def _wrapper(self, *args, **kwargs):
# bound_method has the signature that 'decorator' expects i.e. no
# 'self' argument, but it's a closure over self so it can call
# 'func'. Also, wrap method.__get__() in a function because new
# attributes can't be set on bound method objects, only on functions.
bound_method = wraps(method)(partial(method.__get__(self, type(self))))
for dec in decorators:
bound_method = dec(bound_method)
return bound_method(*args, **kwargs)
grahamd commented on Mar 25, 2022
grahamdmannequin
Mannequin
More actions
Another example in Django, albeit in a test harness.
On the historical front, wraps & update_wrapper were only designed to handle true wrapper functions (i.e. those that don't change the calling signature).
For anything else (including partial), I considered it unlikely that the doc string would still be accurate, let alone any of the other metadata, so I didn't worry about supporting them.
That sais, if it's practical to make the results of combining the two less broken then I agree it would make sense to do so.
For now I just want to fix this bug. I'm in favor of re-engineering the relevant objects so they write their own signature objects, so inspect.Signature doesn't have to understand the internals of objects from other modules. But maybe we save that for another day.
Activity
larryhastings commentedon Feb 15, 2022
It's considered good hygiene to use functools.update_wrapper() to make your wrapped functions look like the original. However, when using functools.partial() to pre-supply arguments to a function, if you then call functools.update_wrapper() to update that partial object, inspect.signature() returns the *original* function's signature, not the *wrapped* function's signature.
To be precise: if you wrap a function with functools.partial() to pre-provide arguments, then immediately call inspect.signature() on that partial object, it returns the correct signature with the pre-filled parameters removed. If you then call functools.update_wrapper() to update the partial from the original function, inspect.signature() now returns the *wrong* signature.
I looked into it a little. The specific thing changing inspect.signature()'s behavior is the '__wrapped__' attribute added by functools.update_wrapper(). By default inspect.signature() will unwrap partial objects, but only if it has a '__wrapped__' attribute.
This all looks pretty deliberate. And it seems like there was some thought given to this wrinkle; inspect.signature() takes a "follow_wrapper_chains" parameter the user can supply to control this behavior. But the default is True, meaning that by default it unwraps partial objects if they have a '__wrapped__'.
I admit I don't have any context for this. Why do we want inspect.signature() to return the wrong signature by default?
13 remaining items
larryhastings commentedon Mar 13, 2022
You make a good point. I filed a separate bug (bpo-46846) suggesting that partial objects should set their own annotations and signature. I agree that objects performing such magic should take care of these details themselves, rather than requiring the inspect module to have workarounds based on deep knowledge of these other modules' inner workings.
larryhastings commentedon Mar 25, 2022
I heard back from both Samuel Colvin (Pydantic) and Sebastián Ramírez (FastAPI). They said neither of them use update_wrapper with partial objects. They also took a peek in a bunch of other projects (FastAPI, Typer, SQLModel, Asyncer, SQLAlchemy, Trio, and AnyIO) and nobody was doing it.
So honestly it seems like nobody (but me!) calls update_wrapper on partial objects, and we can just fix it.
Graham, any final thoughts before we start pulling levers and merging PRs? For now I just want to fix this bug. I'm in favor of re-engineering the relevant objects so they write their own __signature__ objects, so inspect.Signature doesn't have to understand the internals of objects from other modules. But maybe we save that for another day.
grahamd commentedon Mar 25, 2022
It is Django I would worry about and look at closely as they do stuff with decorators on instance methods that uses partials.
https://github.com/django/django/blob/7119f40c9881666b6f9b5cf7df09ee1d21cc8344/django/utils/decorators.py#L43
grahamd commentedon Mar 25, 2022
Another example in Django, albeit in a test harness.
larryhastings commentedon Mar 26, 2022
Ooh, good one. I don't know anybody in the Django project to contact though. Anybody have any leads?
grahamd commentedon Mar 26, 2022
These days I have no idea who is active on Django.
ncoghlan commentedon Mar 26, 2022
On the historical front, wraps & update_wrapper were only designed to handle true wrapper functions (i.e. those that don't change the calling signature).
For anything else (including partial), I considered it unlikely that the doc string would still be accurate, let alone any of the other metadata, so I didn't worry about supporting them.
That sais, if it's practical to make the results of combining the two less broken then I agree it would make sense to do so.
ofey404 commentedon Aug 23, 2022
Does this problem get fixed now? @larryhastings
Sorry for disappearing for so long, but in April I was trapped in Shanghai lockdown and totally forgot this issue…
If this problem is not fixed now, maybe I can post something in Django’s mailing list, trying to come into contact with them?
picnixz commentedon Oct 10, 2025
Please block the above user (see the edit) @python/organization-owners