-
Notifications
You must be signed in to change notification settings - Fork 235
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
Add "Modernizing Superseded Typing Features" guide #1541
Conversation
If this guide is accepted, we can also clean up the "Best Practices" document and cross-link to the modernizing guide. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! A few quick comments.
|
||
*Alternative available since:* Python 3.0; Python 3.12, typing-extensions | ||
|
||
:class:`ByteString <typing.ByteString>` was originally intended to be a type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly this meant is very unclear. This text is probably fine though.
Maybe it's a better idea to name this "Modernizing Superseded Typing Features", as many of those features are not technically deprecated. |
|
||
from typing import TypeAlias # or typing_extensions.TypeAlias | ||
|
||
IntList: TypeAlias = list[int] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might want to also mention typing_extensions.TypeAliasType
, which allows providing a name and explicit typevar order. That’s newer and not all checkers support it yet though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TeamSpen210 As I'm not terribly familiar with TypeAliasType
and the new type
syntax (yet), could you make a suggestion how we can word this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IntList: TypeAlias = list[int] | |
IntList: TypeAlias = list[int] | |
There are cases where `TypeAlias` is insufficient to express what you can express | |
using :keyword:`type`, such as when you want the order of type parameters to | |
be different from how they appear in the body of the alias. | |
In this case you can use `typing_extensions.TypeAliasType` to express this alias | |
in older versions of Python:: | |
from typing_extensions import TypeAliasType, TypeVar | |
T = TypeVar("T", infer_variance=True) | |
ListOrTuple = TypeAliasType("ListOrTuple", list[T] | tuple[T, ...], type_params=(T, )) | |
This is equivalent to:: | |
type ListOrTuple[T] = list[T] | tuple[T, ...] | |
Note however that `TypeAliasType` is newer and not yet supported by every type checker. |
Maybe something like this? I couldn't think of a good example where you'd actually want to change the order of type parameters, but it at least conveys how to use TypeAliasType
to be equivalent with a type
keyword.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I like your text, but I'd like to see an example where this could be useful. I want this document to be a practical guide, so keeping it concise is one of the goals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, until mypy supports TypeAliasType
there might not be much of a point.
I think there's a potential use-case for using an unbound TypeVar
as a slightly more strict gradual type compared to Any
. With the old TypeAlias
it's not possible to have unbound type params unless you pass them back in explicitly, so that could simplify some code, but pyright does not appear to allow unbound type params inside PEP695 type aliases and just treats it as a type parameter anyways if you ignore the error, so you still have to do it the old way and always pass in the type parameter into the alias to leave it unbound.
To illustrate what I mean:
from collections.abc import Callable
from typing import TypeVar
class Foo:
pass
class Bar(Foo):
pass
FooT = TypeVar("FooT", infer_variance=True, bound=Foo)
type AcceptsFoo = Callable[[FooT], object]
# I don't have to specify a type param here, because `AcceptsFoo` takes none
def foo(x: AcceptsFoo) -> None:
return None
def accepts_foo(x: Foo) -> None:
return None
def accepts_bar(x: Bar) -> None:
return None
foo(accepts_foo)
# This would be rejected if FooT was actually bound
# since FooT should be contravariant, but since we
# leave it unbound it behaves like a gradual type
foo(accepts_bar)
# but this will be rejected since `int` is not a subclass of `Foo`
foo(int)
I think this is a better use-case than the explicit order of type params, but since there's no type checker support for it yet, I can't really use it as a motivating example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another reason to use TypeAliasType is for at runtime. Since it's an instance of that type and not just an assignment, using it in type annotations will preserve the alias when introspected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...but since there's no type checker support for [using an unbound type variable] yet...
Using a type variable that is not bound to a scope is expressly forbidden (both in PEP 484 and in PEP 695), so this will never be supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@erictraut That's fair, but if you modify the above example to this:
from collections.abc import Callable
from typing import TypeVar
class Foo:
pass
class Bar(Foo):
pass
FooT = TypeVar("FooT", infer_variance=True, bound=Foo)
type AcceptsFoo[FooT] = Callable[[FooT], object]
def foo(x: AcceptsFoo[FooT]) -> None:
return None
def accepts_foo(x: Foo) -> None:
return None
def accepts_bar(x: Bar) -> None:
return None
foo(accepts_foo)
foo(accepts_bar)
foo(int)
It will work without raising any errors (beyond the last line) and will work in the way I described it, even though FooT
remains unbound in foo
, since it only appears in the function signature once.
To me both of those cases should be semantically equivalent. Using a singular TypeVar
in functions is a common trick to achieve a gradual type like Any
(i.e. it will match in either direction) but that will reject types that don't match the upper bound. But this trick tends to create quite verbose function signatures, which could be avoided if the above were allowed.
Are there any more comments? Otherwise I'd like to merge this. |
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
No description provided.