BUG: change overloads to play nice with pyright.#22193
Conversation
Pyright just chooses the first matching type whenever there is ambiguity in type resolution, which leads to NoReturn for the cross function in certain situations. Other overloads were changed to match. See ticket numpy#22146
|
close/reopen |
|
@BvB93 Thoughts? |
|
Not a typing expert, but I would think the solution is to add another overload for |
|
After a bit more thought I realize this is a terrible solution for many reasons, but still may be a good starting point for dealing with the problem.
I think the issue here is not that there is no matching overload for NDArray[Any], but rather that there are too many. Here's a very straightforward example of what I mean: @overload
def fun(x: int) -> int:
...
@overload
def fun(x: Any) -> float:
...
x: Any = ...
y = fun(x)
reveal_type(y)The problem with this code is that there is no "right" answer for what the type of Some people (like me) would intuitively assume that since there is an explicitly defined overload for arguments of type Mypy takes the more conservative approach and assumes Pyright takes the opposite approach and picks the first matching overload, meaning You might notice the issue with this code snippet is not how the type checkers deal with it, but rather a bug in the code itself. The code in the second overload is telling type checkers "if the input is anything other than an int, we return a float". That's what One option I've thought of is defining a new type that is unused anywhere else, like Some people (see here) have suggested adding the option to delete specific matches from a type to pyhton itself. This would be a neat solution, being able to say "match anything except the types we will define later", but unfortunately it seems to not have gained much traction. Sorry for the wall of text and the non-conclusion, I'll keep thinking about this and see if I can come up with a better solution. In the meantime, feel free to comment with thoughts/suggestions/criticisms. TL;DR: I messed up, the issue is way more complicated than originally assumed, I did come up with something that sort-of-works but I'm not yet sure if it's good. |
BvB93
left a comment
There was a problem hiding this comment.
Right sorry it took some time to respond, and yes: as @iantra has noticed this is a non-trivial issues to fix due to Pyrights questionable way of dealing with overload ambiguity, especially when in the case of ndarray the semantics of the class are so very tightly linked to its (generic) data type.
While I am a bit concerned how viable this is a long term solution (which IMO is something that should be adressed upstream), your _Unknown solution does quite nicely adress the most problematic cases involving NoReturn.
numpy/core/numeric.pyi
Outdated
| class _Unknown: ... | ||
|
|
||
| _ArrayLikeUnknown = Union[ | ||
| _SupportsArray["dtype[_Unknown]"], | ||
| _NestedSequence[_SupportsArray["dtype[_Unknown]"]], | ||
| _Unknown, | ||
| _NestedSequence[_Unknown], | ||
| ] | ||
|
|
There was a problem hiding this comment.
Right, 3 things hre:
- Can you move this to the other ArrayLike type aliases over in numpy/typing/_array_like? (and re-export it to
numpy._typing) - I'd suggest the name
_UnknownTyperather than_Unknown - Could you add a small comment explaining its purpose, maybe in combination with a link to this PR for future reference?
numpy/core/numeric.pyi
Outdated
| @overload | ||
| def outer( | ||
| a: _ArrayLikeObject_co, | ||
| b: _ArrayLikeObject_co, | ||
| out: None = ..., | ||
| ) -> NDArray[object_]: ... |
There was a problem hiding this comment.
This overload should return to its previous position; I assume this is a leftover edit from something?
92e2904 to
4bb480f
Compare
|
Thanks for your time and comments @BvB93, I've gone ahead and fixed everything you mentioned (I think). I do also have my concerns about longevity, especially if pyright decides to change how they handle this at some point, but it's the best I could come up with for now. |
|
Thanks @iantra, LGTM. |
It seems Pyright just chooses the first matching type whenever there is ambiguity in type resolution, which leads to a
NoReturnfornumpy.crossin certain situations (See #22146.) As far as I can tell, there is no agreed-upon behavior or guidelines for dealing with ambiguity in type resolution, so this is valid behavior on Pyright's side (see microsoft/pyright#2521 for a discussion on this topic.)I suppose the ideal solution would be to resolve the ambiguity, but AFAIK there is no way to do that for constructs like
numpy.array([1, 2, 3])on the numpy side, short of asking every user to include a dtype in these cases. I would love to be proven wrong though.I think the second best solution is to have the more general overload also be the first one that matches, so that we make as few assumptions as possible about what might happen inside the function when given an
NDArray[Any]. The other overloads were just changed so that they match the one fornumpy.cross.I would also consider changing the overload order in functions elsewhere in the library, if it makes sense.
I am very open to suggestions if someone has a better idea on how to deal with this.