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
ParamSpec does not consider substitution of @overloads #13540
PEP 612 unfortunately didn't contemplate how ParamSpec would work with overloads, so different type checkers treat it differently. Pyre attempts to capture all overloaded signatures in the ParamSpec and then produce an overloaded result when calculating the specialized return type. This approach works for common cases but falls apart at the edges. Mypy chooses the first overload. Once again, this works for some cases but is probably not what was intended in many cases. Pyright currently disallows the use of overloads with ParamSpec (emits an error) since the behavior is not well defined.
If this is a problem that's worth addressing (and I'm not convinced it is), then someone will probably need to work out all of the intended behaviors (including edge cases), document everything, and drive consensus among the various type checker maintainers.
Could you elaborate on what the edge cases are, for the interaction of ParamSpec and @overload? They're not obvious to me and I couldn't find any discussion of this on the various forums.
(I did find you mentioned this interaction in #12292 (comment), but not that there were unresolved issues and edge cases)
It seems worthwhile to me to address this, since I hit the bug 😄. But perhaps I don't understand the tradeoffs since I don't understand the edge cases.
To identify the edge cases, you will probably need to write a prototype implementation and work through a bunch of test cases that include ParamSpec and Concatenate and a bunch of overloads (including those with function-scoped type variables and ParamSpecs).
I think the approach will necessarily involve solving for not just one set of type variables (as is done with a normal call expression) but n sets, one for each overload in the argument passed to the call. And then once you've solved all n sets of type variables, you'll need to figure out how to combine the results to get the final return type of the call. If the declared return type is a callable itself that uses the ParamSpec, it will presumably translate into a new synthesized overload. If the declared return type isn't a callable, then you will presumably need to apply a union or join from all of the solved sets of type variables to get a combined answer.
It's even worse if the target of the call accepts two callable input parameters that are parameterized by separate ParamSpecs. In that case, if you want to support passing overloaded functions for both parameters, you will need to solve n x m sets of type variables where n is the number of overloads in the first argument and m is the number of overloads in the second.
That's just one issue I can think of, but I'm sure there are many more that I haven't.
I've met this issue while implementing something very similar as the issue description, using starlette's run_in_threadpool which uses ParamSpec. This didn't play well with boto3-stubs which uses @overload to describe the different flavours of boto3.resource. But there is a way around using a lambda closure. This passes:
generic(lambda: f("test"))
and this plays nice because mypy correctly infers the return type of a lambda. Here is a modification of the example with lambda, returning a value and using assert_type to demonstrate that the type is preserved:
FWIW, I worked through all of the issues with ParamSpec capturing an overloaded signature and implemented the support in pyright. There are some limitations, but it works well for all of the real-world use cases that I've encountered. There are several things to keep in mind:
The constraint solver needs to solve constraints independently for each of the applicable overloads. This required a bunch of refactoring in pyright. I'm not sure how much work that would be in mypy.
Overloads need to be filtered based on the signature of the accepting function. For example, if the parameter type is Callable[Concatente[int, P], None] and the argument is an overloaded callable, any overload whose first parameter is not int is not applicable and should be eliminated. Likewise, if the accepting function accepts *args: P.args and **kwargs: P.kwargs, then the overload list should be filtered based on the supplied arguments that map to *args and **kwargs. If this filtering process eliminates all of the overloads, then a type error should be generated. If a single overload remains after filtering, then it's treated like a non-overloaded function. If more than one overload remains, then each overloaded signature gets its own constraint solver context.
If the receiving call returns a callable type that includes the ParamSpec (which is often the case), the result is a new (synthesized) overload composed of all of the individual constraint solutions. If the call returns a non-callable type, then it's not clear which constraint solution should be used. In this case, pyright picks the first constraint solution (i.e. for the first overload in the filtered list produced from step 2). It might also be reasonable to return a union or join of all the solutions. In real-world code, I don't think this ever happens.
Note: Things get very complicated when more than one ParamSpec is involved in the signature because you need to create a constraint solver context for all combinations of ParamSpecs, but this is extremely rare, so it can probably be ignored or treated as an error.
If someone is interested in adding this support to mypy, feel free to reuse the unit tests I wrote for pyright.
Reacted by Alex Waygood, Ilya Priven, Rickey K. Liang, Berserk and Hash
Activity
erictraut commentedon Aug 28, 2022
PEP 612 unfortunately didn't contemplate how
ParamSpecwould work with overloads, so different type checkers treat it differently. Pyre attempts to capture all overloaded signatures in theParamSpecand then produce an overloaded result when calculating the specialized return type. This approach works for common cases but falls apart at the edges. Mypy chooses the first overload. Once again, this works for some cases but is probably not what was intended in many cases. Pyright currently disallows the use of overloads withParamSpec(emits an error) since the behavior is not well defined.If this is a problem that's worth addressing (and I'm not convinced it is), then someone will probably need to work out all of the intended behaviors (including edge cases), document everything, and drive consensus among the various type checker maintainers.
sbrudenell commentedon Aug 28, 2022
Could you elaborate on what the edge cases are, for the interaction of
ParamSpecand@overload? They're not obvious to me and I couldn't find any discussion of this on the various forums.(I did find you mentioned this interaction in #12292 (comment), but not that there were unresolved issues and edge cases)
It seems worthwhile to me to address this, since I hit the bug 😄. But perhaps I don't understand the tradeoffs since I don't understand the edge cases.
erictraut commentedon Aug 29, 2022
To identify the edge cases, you will probably need to write a prototype implementation and work through a bunch of test cases that include
ParamSpecandConcatenateand a bunch of overloads (including those with function-scoped type variables and ParamSpecs).I think the approach will necessarily involve solving for not just one set of type variables (as is done with a normal call expression) but n sets, one for each overload in the argument passed to the call. And then once you've solved all n sets of type variables, you'll need to figure out how to combine the results to get the final return type of the call. If the declared return type is a callable itself that uses the ParamSpec, it will presumably translate into a new synthesized overload. If the declared return type isn't a callable, then you will presumably need to apply a union or join from all of the solved sets of type variables to get a combined answer.
It's even worse if the target of the call accepts two callable input parameters that are parameterized by separate ParamSpecs. In that case, if you want to support passing overloaded functions for both parameters, you will need to solve n x m sets of type variables where n is the number of overloads in the first argument and m is the number of overloads in the second.
That's just one issue I can think of, but I'm sure there are many more that I haven't.
valsteen commentedon Mar 18, 2023
I've met this issue while implementing something very similar as the issue description, using
starlette'srun_in_threadpoolwhich usesParamSpec. This didn't play well withboto3-stubswhich uses@overloadto describe the different flavours ofboto3.resource. But there is a way around using alambdaclosure. This passes:and this plays nice because mypy correctly infers the return type of a
lambda. Here is a modification of the example with lambda, returning a value and usingassert_typeto demonstrate that the type is preserved:https://mypy-play.net/?mypy=latest&python=3.10&gist=1f323faa2788a034a665dd4052d1e51a
erictraut commentedon Mar 18, 2023
FWIW, I worked through all of the issues with
ParamSpeccapturing an overloaded signature and implemented the support in pyright. There are some limitations, but it works well for all of the real-world use cases that I've encountered. There are several things to keep in mind:Callable[Concatente[int, P], None]and the argument is an overloaded callable, any overload whose first parameter is notintis not applicable and should be eliminated. Likewise, if the accepting function accepts*args: P.argsand**kwargs: P.kwargs, then the overload list should be filtered based on the supplied arguments that map to*argsand**kwargs. If this filtering process eliminates all of the overloads, then a type error should be generated. If a single overload remains after filtering, then it's treated like a non-overloaded function. If more than one overload remains, then each overloaded signature gets its own constraint solver context.Note: Things get very complicated when more than one ParamSpec is involved in the signature because you need to create a constraint solver context for all combinations of ParamSpecs, but this is extremely rare, so it can probably be ignored or treated as an error.
If someone is interested in adding this support to mypy, feel free to reuse the unit tests I wrote for pyright.
Restore compose(list, product) with linked type: ignores