-
Notifications
You must be signed in to change notification settings - Fork 237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AnyOf - Union for return types #566
Comments
Another use case - at least util we get a more flexible StartResponse = AnyOf[
Callable[[str, List[(str, str)]], None],
Callable[[str, List[(str, str)], ExcInfo], None]
]
def create_start_response() -> StartResponse:
...
create_start_response()("200 OK", []) Using |
This would be as unsafe as Similarly we won't know if |
Good to know that there is a proper solution for the callback case! Personally, I think the improvement in type safety over just returning |
In my experience I never had a situation where I needed unsafe unions. Anyway, I could imagine some people might want it. However, the problem is that the benefits of unsafe unions are incomparable with the amount of work to implement them. Adding a new kind of types to mypy e.g. could take months of intense work. This one will be as hard as introducing intersection types, and while the later are more powerful (they cover most of use cases of unsafe unions) we still hesitate to start working on it. |
I'm with Ivan, and this is something we've considered earlier -- see the discussion at python/mypy#1693, for example. The relatively minor benefits don't really seem worth the extra implementation work and complexity. This would only be a potentially good idea for legacy APIs that can't be typed precisely right now, and the most popular of those can be special cased by tools (e.g. through mypy plugins). Mypy already special cases Ad-hoc extensions have the benefit of being easy to implement. They are also modular, don't complicate the rest of the type system, and they potentially allow inferring precise return types. There is also often a simple workaround -- write a wrapper function around the library function with an def open_text(path: str, mode: str) -> TextIO:
assert 'b' not in mode
return open(path, mode)
def open_binary(path: str, mode: str) -> BinaryIO:
assert 'b' not in mode
return open(path, mode + 'b') |
Some use cases (such as |
This continues to crop up with typeshed pretty regularly. For a lot of return types, typeshed either has to either make a pretty opinionated decision or completely forgo type safety with Any. One use case I find pretty compelling is for autocomplete in IDEs, eg: python/typeshed#4511 From a typeshed perspective, it would be nice to support these use cases. From a mypy perspective, I agree it's maybe not worth the effort, so maybe type checkers could interpret a potential AnyOf as a plain Any. |
Maybe you could bring this up on typing-sig? A proto-pep might get support there. Or maybe you can spell this using |
I brought this up on typing-sig. |
Semantically, would |
Guido's thoughts on the subject: https://mail.python.org/archives/list/[email protected]/message/TTPVTIKZ6BFVWZBUYR2FN2SPGB63Z7PH/ There's probably also some slightly different behaviour when intersecting slightly incompatible types. E.g., for an intersection maybe you'd want to treat intersection order like an MRO, but for AnyOf you'd probably want "is compatible with any of the intersection" |
I see, thanks for reminding me of that email! I suppose this matters when you're implementing a function with an def some_func() -> Intersection[bytes, str]: ... And it would work as expected. But when implementing it, you'd write: def some_func() -> Intersection[bytes, str]:
if something:
return bytes()
else:
return str() And a type checker would flag the return type as incompatible. So in Guido's terminology, AnyOf would have to behave like Union in a "receiving" position and like Intersection in a "giving" position. |
Maybe I'm misunderstanding how intersections are supposed to work, but to me an intersection type is a type that fulfills the protocol of multiple other types. At least that's how e.g. typescript and the Ivan's initial comment in #213 describe it. |
And that means that def foo(b: bytes): ...
x: AnyOf[str, bytes]
y: str | bytes
foo(x) # ok
foo(y) # error |
And sorry for the spam, but one last thought: For the caller of a function, there is no difference, whether an argument is annotated with |
It would be for the benefit of the callee. Conversely, for the callee there's no reason to return an AnyOf, since for them a Union works as well. Taking your example, the connection between AnyOf and Intersection is that if we had
would work as well. But presumably, to give x a value, you'd want to work either of these:
And there it behaves like Union. Combining this, we can have:
At this point I would just start repeating what I said in that email, so I'll stop here. |
How would I ask because of complex cases like this: python/typeshed#9461 from PyQt6.QtGui import QImage as PyQt6_QImage # type: ignore[import]
from PyQt5.QtGui import QImage as PyQt5_QImage # type: ignore[import]
from PySide6.QtGui import QImage as PySide6_QImage # type: ignore[import]
from PySide2.QtGui import QImage as PySide2_QImage # type: ignore[import]
def foo() -> AnyOf[PyQt6_QImage, PyQt5_QImage, PySide6_QImage, PySide2_QImage]: ... Or in a non-stub file with type inference: try:
from PyQt6.QtGui import QImage # type: ignore[import]
except:
pass
try:
from PyQt5.QtGui import QImage # type: ignore[import]
except:
pass
try:
from PySide6.QtGUI import QImage # type: ignore[import]
except:
pass
try:
from PySide2.QtGUI import QImage # type: ignore[import]
except:
pass
# If inference is not feasible. An explicit AnyOf return type like a bove would do.
def foo():
return QImage() |
Thoughts for a different approach: This way you can keep annotating the types accurately. No need for a new user-facing type to juggle with. And let the users choose whether they want total strictness or be more permissive. Pytype tends to err on the permissive side. Mypy can probably already be done with a plugin. |
Sometimes you want to return the usual union: if a function returns |
Sometimes a function or method can return one of several types, depending on the passed in arguments or external factors. Best example is probably
open()
, but there are other examples in the standard library, likeshutil.copy()
[1]. In many of those cases, the caller knows what return type to expect.Currently there are several options, but none of them is really satisfactory:
@overload
. This is the best solution if it can be used. But that is often not the case, like in the examples above.Union
as the return type. This is usually not recommended, since it means that the caller needs to useisinstance()
to use the return type.Any
as the return type. This is currently best practice in those cases, but of course provides no type safety at all.Therefore, I propose to add another type, for example
AnyOf[...]
that acts likeUnion
, but can be used everywhere any of its type arguments could be used.I also think that the documentation should make it clear that using
AnyOf
is a code smell.[1] Currently the type behaviour in
shutil
is broken in my opinion, but that does not change the fact that currently it is as it is.The text was updated successfully, but these errors were encountered: