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.cache destroys the function signature #11280
I think the conclusion we reached in previous attempts was that ParamSpec is unsufficient to solve this, due to the complex interaction with classmethod.
I think the best bet for getting stuff like this working is a potential future Intersection construct, to extend the Callable that's passed in with the additional attributes, rather than try to implement a descriptor that works transparently in regular functions, methods and classmethods, which does not appear to be possible.
I'm confused. Is there something about functools.cache that makes it particularly difficult to type, or does every function decorator have this problem?
I noticed functools.wraps also isn't annotated correctly:
contextlib.contextmanager takes a callable and returns another callable that is in no way distinguished from most other callables. This makes it pretty easy to type:
functools.lru_cache and functools.cache, on the other hand, take a callable and return a custom callable object (an instance of functools._lru_cache_wrapper) that also has several special methods present on it (cache_info(), cache_clear() and cache_parameters()):
Unfortunately, this does indeed make it quite a bit harder to type these particular decorators than contextmanager. Ideally we'd just have _lru_cache_wrapper be generic over a ParamSpec as well as the return type, but this isn't as easy as it sounds. There are ambiguities in the spec regarding how ParamSpecs should be captured with decorated methods that mean that this results in false positives on user code. See #6356.
If a perfect solution isn't possible, I'd strongly prefer a solution that gives me useful IntelliSense.
I agree the current situation is deeply unsatisfactory (I think we all do); it's just a question of whether we can find a solution that doesn't cause an unacceptable number of false positives when type checkers are run on user code.
FWIW I think #7771 came close, and I'd be interested in looking at that approach again (though I don't have time right now)
Wait, contextmanager returns a Callable? But it can still be used on methods? So Callable doesn't mean "it's callable", it actually means "it's callable and is a descriptor that returns a bound method"? That's... odd.
@Aran-FeyCallable is special cased inside a class, so foo: Callable[[int], None] behaves (almost) the same as if you had written def foo(self, x: int, /) -> None: ...
The almost concerns calling foo from the class instead of an instance, where the two differ, the former will expect you to pass in an instance of the class, while the latter will not.
But things are yet again different when you use a decorator and return a Callable with the same signature from that, in that case it will actually be equivalent to the def case.
All of this special casing is also part of what makes it so difficult to write a descriptor that transparently works the same way.
While this fixes the incorrect signature, you lose the extra attributes that functools.cache introduces this way, so it's at best trading one problem for another. But it can be a reasonable workaround in code that does not care about the presence of cache_clear/cache_info/cache_parameters.
While this fixes the incorrect signature, you lose the extra attributes that functools.cache introduces this way, so it's at best trading one problem for another.
In fact there's no way to have both the extra methods and the correct signature at the same time with the current type stubs, since _lru_cache_wrapper hard codes them to *args: Hashable, **kwargs: Hashable and I postulate there's no way to frame this as a descriptor using ParamSpec either, due to the special-casing for classmethod, so intersection remains our only hope for solving this at this point, unless someone can demonstrate a solution that works in every possible case.
In pyright/pylance you actually have the worst of both worlds with your fix, because you both erase the function signature and _lru_cache_wrapper. So you don't get the correct signature and you also can't access the extra methods.
Perhaps the reason you were fooled into thinking this was a fix for the issue, is due to the fact that Jedi or any LSP built on top of it would show the correct function signature this way. But type checkers still infer the wrong signature.
Also just so we're clear, when I say "there's no way to make this work" I purely am talking about from a type checking perspective. At runtime this will work, but runtime isn't the issue here. Also you can work around this with your own custom descriptor that doesn't need to be able to handle functions, methods and classmethods all at the same time, if you split these into separate descriptors you can make it work right now, but we don't have this luxury with the standard library.
I strongly agree with @Aran-Fey that until this is solved, it would be much better to preserve the function signature at the cost of not checking that parameters are hashable than what is currently there.
If you decorate a function that has non-hashable parameters, you'll have a runtime error the first time you run your code.
With the current state, you can pass wrong parameter types to your function possibly resulting in much subtler/harder to notice bugs.
I strongly agree with @Aran-Fey that until this is solved, it would be much better to preserve the function signature at the cost of not checking that parameters are hashable than what is currently there.
Unfortunately that is not the only cost. The additional cost and the main reason this is annotated the way it currently is, is that we will see false positives anywhere people try to call cache_clear/cache_info/cache_parameters. This will generate a lot of noise and confusion. Typeshed generally prefers false negatives over false positives.
It would be easy to preserve the function signature if we didn't need to make sure those methods are available.
In pyright/pylance you actually have the worst of both worlds with your fix, because you both erase the function signature and _lru_cache_wrapper. So you don't get the correct signature and you also can't access the extra methods.
Perhaps the reason you were fooled into thinking this was a fix for the issue, is due to the fact that Jedi or any LSP built on top of it would show the correct function signature this way. But type checkers still infer the wrong signature.
@Daverball My issue was that functools.cache disabled Vscode's type and docstring hints, which my fix reenables. I use Pylance not Jedi and have not experienced any issues. However, it seems that this would not suffice for your use cases and regardless I would like to see this fixed without the need for workarounds.
I'm aware there's a (slightly stale) discussion about changing ParamSpec to support this but I got a version of cache that works as desired in the examples I came up with. Mypy doesn't error with an unhashable argument for both my version and the builtin version but seems to give correct errors in all the other examples I had. It'll have false positives if someone has a method or classmethod without self or cls as the first argument, or vice versa. I'm ok with that. To get lru_cache working the int | None overload needs to return a protocol that has 3 __call__ overloads that are basically the same as the cache overloads below.
When mypy does corrrectly error it gives a slightly strange error, eg:
main.py:164: error: Too many arguments for "__call__" of "FunctionDescriptor" [call-arg]
main.py:164: note: Looks like the first special argument in a method is not named "self", "cls", or "mcs", maybe it is missing?
But otherwise seems to work, if this is of interest to others.
It still doesn't preserve the docstring in intellisense but it avoids any obvious type errors at least.
My intellisense autocomplete suggests kwds as a function parameter. Not sure why. But I wouldn't call that a blocker here, considering the status quo. The built in cache version results in it suggesting kwargs similarily. But with my version it also suggests the correct arguments (and errors if kwds is used even though it's suggested).
If someone can find an example where this doesn't work lemme know. Or I can rewrite it using the older TypeVar syntax if a PR would be of interest.
I am going to close this as "inexpressible" for now. There have been multiple tries to fix this, but none have been successful. It seems that we need changes to Python's type system before we can solve this.
The types shipped with cachetools as of tkem/cachetools#398 seem to solve this situation, so maybe this is a good time to revisit this?
In all honesty I didn't take the time to dig into how functools.cache is annotated, but it does basically the same thing as cacheutils.func.lru_cache, so if it's possible to express one, then it should now also be possible to express the other. Maybe.
(I also didn't deeply think about whether the cacheutils one is fully correct, but it seems to work OK on some simple tests)
Activity
srittau commentedon Jan 16, 2024
This should now be expressible using
ParamSpec.AlexWaygood commentedon Jan 16, 2024
See also:
functools: Add typing for decorated function arguments in cache API #9768Daverball commentedon Jan 18, 2024
I think the conclusion we reached in previous attempts was that
ParamSpecis unsufficient to solve this, due to the complex interaction withclassmethod.I think the best bet for getting stuff like this working is a potential future
Intersectionconstruct, to extend theCallablethat's passed in with the additional attributes, rather than try to implement a descriptor that works transparently in regular functions, methods and classmethods, which does not appear to be possible.Aran-Fey commentedon Jan 18, 2024
I'm confused. Is there something about
functools.cachethat makes it particularly difficult to type, or does every function decorator have this problem?I noticed
functools.wrapsalso isn't annotated correctly:But
contextlib.contextmanageris correct:What gives? Why are some decorators correct and some incorrect/impossible?
Also, allow me to repeat/rephrase what I said earlier:
If a perfect solution isn't possible, I'd strongly prefer a solution that gives me useful IntelliSense.
AlexWaygood commentedon Jan 18, 2024
contextlib.contextmanagertakes a callable and returns another callable that is in no way distinguished from most other callables. This makes it pretty easy to type:typeshed/stdlib/contextlib.pyi
Line 76 in c51de8e
functools.lru_cacheandfunctools.cache, on the other hand, take a callable and return a custom callable object (an instance offunctools._lru_cache_wrapper) that also has several special methods present on it (cache_info(),cache_clear()andcache_parameters()):typeshed/stdlib/functools.pyi
Line 201 in c51de8e
Unfortunately, this does indeed make it quite a bit harder to type these particular decorators than
contextmanager. Ideally we'd just have_lru_cache_wrapperbe generic over aParamSpecas well as the return type, but this isn't as easy as it sounds. There are ambiguities in the spec regarding howParamSpecs should be captured with decorated methods that mean that this results in false positives on user code. See #6356.I agree the current situation is deeply unsatisfactory (I think we all do); it's just a question of whether we can find a solution that doesn't cause an unacceptable number of false positives when type checkers are run on user code.
FWIW I think #7771 came close, and I'd be interested in looking at that approach again (though I don't have time right now)
Aran-Fey commentedon Jan 18, 2024
Wait,
contextmanagerreturns aCallable? But it can still be used on methods? SoCallabledoesn't mean "it's callable", it actually means "it's callable and is a descriptor that returns a bound method"? That's... odd.Daverball commentedon Jan 18, 2024
@Aran-Fey
Callableis special cased inside a class, sofoo: Callable[[int], None]behaves (almost) the same as if you had writtendef foo(self, x: int, /) -> None: ...The almost concerns calling
foofrom the class instead of an instance, where the two differ, the former will expect you to pass in an instance of the class, while the latter will not.But things are yet again different when you use a decorator and return a
Callablewith the same signature from that, in that case it will actually be equivalent to thedefcase.All of this special casing is also part of what makes it so difficult to write a descriptor that transparently works the same way.
Aran-Fey commentedon Jan 18, 2024
I see, thanks! But boy, what a mess.
umarbutler commentedon Mar 11, 2024
You can fix this by running:
Instead of:
Daverball commentedon Mar 11, 2024
While this fixes the incorrect signature, you lose the extra attributes that
functools.cacheintroduces this way, so it's at best trading one problem for another. But it can be a reasonable workaround in code that does not care about the presence ofcache_clear/cache_info/cache_parameters.umarbutler commentedon Mar 11, 2024
Those all still work for me...
Daverball commentedon Mar 11, 2024
I assumed this would fall under no type reassignments, but it looks like there's special casing to make this equivalent to decorating the function, but it also looks like your fix doesn't actually preserve the function signature, at least not in mypy or pyright:
https://mypy-play.net/?mypy=latest&python=3.12
https://pyright-play.net/?pythonVersion=3.12&code=JYWwDg9gTgLgBAMwK4DsDGMIQDYGcCwAUEQCYCmCcIAngPrLozAQoAUAlHALQB8cAcizIAuInHFwwAQ1wFihGvVQZmKOAF5EyzDlwA6AO5QpYXK0UMVLdq0s68etFLQALMubp3V7dkShkANzIpbFoYajB3C21vP0Dg0PDIjyVGVT1aWidsUNp2IA
In fact there's no way to have both the extra methods and the correct signature at the same time with the current type stubs, since
_lru_cache_wrapperhard codes them to*args: Hashable, **kwargs: Hashableand I postulate there's no way to frame this as a descriptor usingParamSpeceither, due to the special-casing forclassmethod, so intersection remains our only hope for solving this at this point, unless someone can demonstrate a solution that works in every possible case.In pyright/pylance you actually have the worst of both worlds with your fix, because you both erase the function signature and
_lru_cache_wrapper. So you don't get the correct signature and you also can't access the extra methods.Perhaps the reason you were fooled into thinking this was a fix for the issue, is due to the fact that Jedi or any LSP built on top of it would show the correct function signature this way. But type checkers still infer the wrong signature.
Also just so we're clear, when I say "there's no way to make this work" I purely am talking about from a type checking perspective. At runtime this will work, but runtime isn't the issue here. Also you can work around this with your own custom descriptor that doesn't need to be able to handle functions, methods and classmethods all at the same time, if you split these into separate descriptors you can make it work right now, but we don't have this luxury with the standard library.
ldorigo commentedon Mar 13, 2024
I strongly agree with @Aran-Fey that until this is solved, it would be much better to preserve the function signature at the cost of not checking that parameters are hashable than what is currently there.
If you decorate a function that has non-hashable parameters, you'll have a runtime error the first time you run your code.
With the current state, you can pass wrong parameter types to your function possibly resulting in much subtler/harder to notice bugs.
Daverball commentedon Mar 13, 2024
Unfortunately that is not the only cost. The additional cost and the main reason this is annotated the way it currently is, is that we will see false positives anywhere people try to call
cache_clear/cache_info/cache_parameters. This will generate a lot of noise and confusion. Typeshed generally prefers false negatives over false positives.It would be easy to preserve the function signature if we didn't need to make sure those methods are available.
umarbutler commentedon Mar 17, 2024
@Daverball My issue was that
functools.cachedisabled Vscode's type and docstring hints, which my fix reenables. I use Pylance not Jedi and have not experienced any issues. However, it seems that this would not suffice for your use cases and regardless I would like to see this fixed without the need for workarounds.alwaysmpe commentedon Nov 3, 2024
I'm aware there's a (slightly stale) discussion about changing
ParamSpecto support this but I got a version ofcachethat works as desired in the examples I came up with. Mypy doesn't error with an unhashable argument for both my version and the builtin version but seems to give correct errors in all the other examples I had. It'll have false positives if someone has a method or classmethod withoutselforclsas the first argument, or vice versa. I'm ok with that. To getlru_cacheworking theint | Noneoverload needs to return a protocol that has 3__call__overloads that are basically the same as thecacheoverloads below.When mypy does corrrectly error it gives a slightly strange error, eg:
But otherwise seems to work, if this is of interest to others.
It still doesn't preserve the docstring in intellisense but it avoids any obvious type errors at least.
My intellisense autocomplete suggests
kwdsas a function parameter. Not sure why. But I wouldn't call that a blocker here, considering the status quo. The built in cache version results in it suggestingkwargssimilarily. But with my version it also suggests the correct arguments (and errors ifkwdsis used even though it's suggested).If someone can find an example where this doesn't work lemme know. Or I can rewrite it using the older
TypeVarsyntax if a PR would be of interest.theory: change _get_chords_from_notes notes parameter type to Iterobla
Use ParamSpec _lru_cache_wrapper
srittau commentedon Sep 23, 2025
I am going to close this as "inexpressible" for now. There have been multiple tries to fix this, but none have been successful. It seems that we need changes to Python's type system before we can solve this.
theory: change _get_chords_from_notes notes parameter type to Iterobla
Add tests for @functools.cache (#14768)
zoltan-axle commentedon May 26, 2026
The types shipped with
cachetoolsas of tkem/cachetools#398 seem to solve this situation, so maybe this is a good time to revisit this?In all honesty I didn't take the time to dig into how
functools.cacheis annotated, but it does basically the same thing ascacheutils.func.lru_cache, so if it's possible to express one, then it should now also be possible to express the other. Maybe.(I also didn't deeply think about whether the
cacheutilsone is fully correct, but it seems to work OK on some simple tests)srittau commentedon May 26, 2026
An exploratory PR (with tests for the corner cases described here and in the related issues and PRs) would certainly be welcome.