From b5a8c5fffa77540bafbce8c3b48d1b3c20876118 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Thu, 14 Jul 2022 23:55:10 +0100 Subject: [PATCH 01/13] Initial Type defaults for TypeVars PEP --- .github/CODEOWNERS | 1 + pep-9999.rst | 394 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 pep-9999.rst diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index eb6b426fafa..efb92d0d348 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -576,6 +576,7 @@ pep-0692.rst @jellezijlstra pep-0693.rst @Yhg1s pep-0694.rst @dstufft pep-0695.rst @gvanrossum +pep-9999.rst @jellezijlstra # ... # pep-0754.txt # ... diff --git a/pep-9999.rst b/pep-9999.rst new file mode 100644 index 00000000000..549a5031bb3 --- /dev/null +++ b/pep-9999.rst @@ -0,0 +1,394 @@ +PEP: 9999 +Title: Type defaults for TypeVars +Version: $Revision$ +Last-Modified: $Date$ +Author: James Hilton-Balfe +Sponsor: Jelle Zijlstra +Discussions-To: typing-sig@python.org +Status: Draft +Type: Standards Track +Topic: Typing +Content-Type: text/x-rst +Created: 14-Jul-2022 +Python-Version: 3.12 + +Type defaults for TypeVars +========================== + +Abstract +-------- + +This PEP introduces the concept of type defaults for TypeVars, which act +as defaults for a type parameter when none is specified. + +Motivation +---------- + +.. code:: py + + T = TypeVar("T", default=int) # This means that if no type is specified T = int + + @dataclass + class Box(Generic[T]): + value: T | None = None + + reveal_type(Box()) # type is Box[int] + reveal_type(Box(value="Hello World!")) # type is Box[str] + +One place this `regularly comes +up `__ is ``Generator``. I +propose changing the *stub definition* to something like: + +.. code:: py + + YieldT = TypeVar("YieldT") + SendT = TypeVar("SendT", default=None) + ReturnT = TypeVar("ReturnT", default=None) + + class Generator(Generic[YieldT, SendT, ReturnT]): ... + + Generator[int] == Generator[int, None] == Generator[int, None, None] + +This is also useful for a Generic that is commonly over one type. + +.. code:: py + + class Bot: ... + + BotT = TypeVar("BotT", bound=Bot, default=Bot) + + class Context(Generic[BotT]): + bot: BotT + + class MyBot(Bot): ... + + reveal_type(Context().bot) # type is Bot # notice this is not Any which is what it would be currently + reveal_type(Context[MyBot]().bot) # type is MyBot + +Not only does this improve typing for those who explicitly use it. It +also helps non-typing users who rely on auto-complete to speed up their +development. + +This design pattern is common in projects like: + - `discord.py `__ - where the + example above was taken from. + - `NumPy `__ - the default for types + like ``ndarray``\ 's ``dtype`` would be ``float64``. Currently it's + ``Unkwown`` or ``Any``. + - `TensorFlow `__ (this + could be used for Tensor similarly to ``numpy.ndarray`` and would be + useful to simplify the definition of ``Layer``) + +`This proposal could also be used on +``builtins.slice`` `__ +where the parameter should start default to int, stop default to start +and step default to ``int | None`` + +.. code:: py + + StartT = TypeVar("StartT", default=int) + StopT = TypeVar("StopT", default=StartT) + StepT = TypeVar("StepT", default=int | None) + + class slice(Generic[StartT, StopT, StepT]): ... + +Specification +------------- + +Default ordering and subscription rules +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The order for defaults should follow the standard function parameter +rules, so a ``TypeVar`` with no ``default`` cannot follow one with a +``default`` value. Doing so should ideally raise a ``TypeError`` in +``typing._GenericAlias``/``types.GenericAlias``, and a type checker +should flag this an error. + +.. code:: py + + DefaultStrT = TypeVar("DefaultStrT", default=str) + DefaultIntT = TypeVar("DefaultIntT", default=int) + DefaultBoolT = TypeVar("DefaultBoolT", default=bool) + T = TypeVar("T") + T2 = TypeVar("T2") + + class NonDefaultFollowsDefault(Generic[DefaultStrT, T]): ... # Invalid: non-default TypeVars cannot follow ones with defaults + + + class NoNonDefaults(Generic[DefaultStrT, DefaultIntT]): ... + + ( + NoNoneDefaults == + NoNoneDefaults[str] == + NoNoneDefaults[str, int] + ) # All valid + + + class OneDefault(Generic[T, DefaultBoolT]): ... + + OneDefault[float] == OneDefault[float, bool] # Valid + + + class AllTheDefaults(Generic[T1, T2, DefaultStrT, DefaultIntT, DefaultBoolT]): ... + + AllTheDefaults[int] # Invalid: expected 2 arguments to AllTheDefaults + ( + AllTheDefaults[int, complex] == + AllTheDefaults[int, complex, str] == + AllTheDefaults[int, complex, str, int] == + AllTheDefaults[int, complex, str, int, bool] + ) # All valid + +This cannot be enforce at runtime for functions, for now, but in the +future, this might be possible (see `Interaction with PEP +695 <#interaction-with-pep-695>`__). + +``Generic`` ``TypeAlias``\ es +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Generic`` ``TypeAlias``\ es should be able to be further subscripted +following normal subscription rules. If a ``TypeVar`` has a default +that hasn't been overridden it should be treated like it was +substituted into the ``TypeAlias``. However, it can be specialised +further down the line. + +.. code:: py + + class SomethingWithNoDefaults(Generic[T, T2]): ... + + MyAlias: TypeAlias = SomethingWithNoDefaults[int, DefaultStrT] # valid + reveal_type(MyAlias) # type is SomethingWithNoDefaults[int, str] + reveal_type(MyAlias[bool]) # type is SomethingWithNoDefaults[int, bool] + + MyAlias[bool, int] # Invalid: too many arguments passed to MyAlias + +Subclassing +~~~~~~~~~~~ + +Subclasses of ``Generic``\ s with ``TypeVar``\ s that have defaults +behave similarly to ``Generic`` ``TypeAlias``\ es. + +.. code:: py + + class SubclassMe(Generic[T, DefaultStrT]): ... + + class Bar(SubclassMe[int, DefaultStrT]): ... + reveal_type(Bar) # type is Bar[str] + reveal_type(Bar[bool]) # type is Bar[bool] + + class Foo(SubclassMe[int]): ... + + reveal_type(Spam) # type is + Foo[str] # Invalid: Foo cannot be further subscripted + + class Baz(Generic[DefaultIntT, DefaultStrT]): ... + + class Spam(Baz): ... + reveal_type(Spam) # type is + +Using bound and default +~~~~~~~~~~~~~~~~~~~~~~~ + +If both ``bound`` and ``default`` are passed ``default`` must be a +subtype of ``bound``. + +.. code:: py + + TypeVar("Ok", bound=float, default=int) # Valid + TypeVar("Invalid", bound=str, default=int) # Invalid: the bound and default are incompatible + +Constraints +~~~~~~~~~~~ + +For constrained ``TypeVar``\ s, the default needs to be one of the +constraints. It would be an error even if it is a subtype of one of the +constraints. + +.. code:: py + + TypeVar("Ok", float, str, default=float) # Valid + TypeVar("Invalid", float, str, default=int) # Invalid: expected one of float or str got int + +Function Defaults +~~~~~~~~~~~~~~~~~ + +The ``TypeVar``\ 's default should also be compatible with the +parameter's runtime default if present. But they are erased to +not have defaults when called. + +Implementation +-------------- + +At runtime, this would involve the following changes to +``typing.TypeVar``: + +- the type passed to default would be available as a ``__default__`` + attribute. + +The following changes would be required to both ``GenericAlias``\ es: + +- logic to determine the defaults required for a subscription. + + - potentially a way construct ``types.GenericAliases`` using a + classmethod to allow for defaults in + ``__class_getitem__ = classmethod(GenericAlias)`` + i.e. ``GenericAlias.with_type_var_likes()``. + + .. code:: py + + # _collections_abc.py + + _sentinel = object() + + # NOTE: this is not actually typing.TypeVar, that's in typing.py, + # this is just to trick is_typevar() in genericaliasobject.c + class TypeVar: + __module__ = "typing" + + def __init__(self, name, *, default=_sentinel): + self.__name__ = name + self.__default__ = default + + YieldT = TypeVar("YieldT") + SendT = TypeVar("SendT", default=None) + ReturnT = TypeVar("ReturnT", default=None) + + class Generator(Iterable): + __class_getitem__ = GenericAlias.with_type_var_likes(YieldT, SendT, ReturnT) + +- ideally, logic to determine if subscription (like + ``Generic[T, DefaultT]``) would be valid. + +A reference implementation of the above can be found at +https://github.com/Gobot1234/mypy/tree/TypeVar-defaults + +Interaction with :pep:`695` +--------------------------- + +If this PEP were to be accepted, amendments to PEP 695 could be made to +allow for specifying defaults for type parameters using the new syntax. +Specifying a default should be done using the “=” operator inside of the +square brackets like so: + +.. code:: py + + class Foo[T = str]: ... + + def bar[U = int](): ... + +This functionality was included in the initial draft of PEP 695 but was +removed due to scope creep. + +Grammar Changes +~~~~~~~~~~~~~~~ + +:: + + type_param: + | a=NAME b=[type_param_bound] d=[type_param_default] + | a=NAME c=[type_param_constraint] d=[type_param_default] + | '*' a=NAME d=[type_param_default] + | '**' a=NAME d=[type_param_default] + + type_param_default: '=' e=expression + +This would mean that ``TypeVarLikes`` with defaults proceeding those +with non-defaults can be checked at compile time. Although this version +of the PEP does not define behaviour for ``TypeVarTuple`` and +``ParamSpec`` defaults, this would mean they can be added easily in the +future. + +Rejected Alternatives +--------------------- + +Specification for ``TypeVarTuple`` and ``ParamSpec`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An older version of this PEP included a specification for +``TypeVarTuple`` and ``ParamSpec`` defaults. However, this has been +removed as few practical use cases for the two were found. Maybe this +can be revisited. + +Allowing the ``TypeVar`` defaults to be passed to ``type.__new__``\ 's ``**kwargs`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: py + + T = TypeVar("T") + + @dataclass + class Box(Generic[T], T=int): + value: T | None = None + +While this is much easier to read and follows a similar rationale to the +``TypeVar`` `unary +syntax `__, it would not be +backwards compatible as ``T`` might already be passed to a +metaclass/superclass or support classes that don't subclass ``Generic`` +at runtime. + +Ideally, if :pep:`637` wasn't rejected, the following would be acceptable: + +.. code:: py + + T = TypeVar("T") + + @dataclass + class Box(Generic[T = int]): + value: T | None = None + +Allowing non-defaults to follow defaults +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: py + + YieldT = TypeVar("YieldT", default=Any) + SendT = TypeVar("SendT", default=Any) + ReturnT = TypeVar("ReturnT") + + class Coroutine(Generic[YieldT, SendT, ReturnT]): ... + + Coroutine[int] == Coroutine[Any, Any, int] + +Allowing non-defaults to follow defaults would alleviate the issues with +returning types like ``Coroutine`` from functions where the most used +type argument is the last (the return). Allowing non-defaults to follow +defaults is too confusing and potentially ambiguous, even if only the +above two forms were valid. Changing the argument order now would also +break a lot of codebases. This is also solvable in most cases using a +``TypeAlias``. + +.. code:: py + + Coro: TypeAlias = Coroutine[Any, Any, T] + Coro[int] == Coroutine[Any, Any, int] + +Having ``default`` implicitly be ``bound`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In an earlier version of this PEP, the ``default`` was implicitly the +to ``bound`` if no value was passed for ``default``. This while +convenient, could have a ``TypeVar`` with no default follow a +``TypeVar`` with a default. Consider: + +.. code:: py + + T = TypeVar("T", bound=int) # default is implicitly int + U = TypeVar("U") + + class Foo(Generic[T, U]): + ... + + # would expand to + + T = TypeVar("T", bound=int, default=int) + U = TypeVar("U") + + class Foo(Generic[T, U]): + ... + + +Thanks to the following people for their feedback on the PEP: + +Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morgan +and Jakub Kuczys \ No newline at end of file From 19d06fa20ca710ec644c1278e864e5ce5410af18 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sat, 16 Jul 2022 02:23:51 +0100 Subject: [PATCH 02/13] I don't think the pep role works in headers --- pep-9999.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pep-9999.rst b/pep-9999.rst index 549a5031bb3..6b123b1e50e 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -262,8 +262,8 @@ The following changes would be required to both ``GenericAlias``\ es: A reference implementation of the above can be found at https://github.com/Gobot1234/mypy/tree/TypeVar-defaults -Interaction with :pep:`695` ---------------------------- +Interaction with PEP 695 +------------------------ If this PEP were to be accepted, amendments to PEP 695 could be made to allow for specifying defaults for type parameters using the new syntax. From d051a2eed40239a369910209b2a0b9d24e750930 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sat, 16 Jul 2022 10:54:17 +0100 Subject: [PATCH 03/13] Fix some more typos --- pep-9999.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pep-9999.rst b/pep-9999.rst index 6b123b1e50e..ff20a0aec71 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -74,15 +74,14 @@ This design pattern is common in projects like: example above was taken from. - `NumPy `__ - the default for types like ``ndarray``\ 's ``dtype`` would be ``float64``. Currently it's - ``Unkwown`` or ``Any``. + ``Unknown`` or ``Any``. - `TensorFlow `__ (this could be used for Tensor similarly to ``numpy.ndarray`` and would be useful to simplify the definition of ``Layer``) -`This proposal could also be used on -``builtins.slice`` `__ -where the parameter should start default to int, stop default to start -and step default to ``int | None`` +`This proposal could also be used on builtins.slice `__ +where the parameter should ``start`` default to ``int``, ``stop`` +default to ``start`` and step default to ``int | None`` .. code:: py @@ -259,7 +258,7 @@ The following changes would be required to both ``GenericAlias``\ es: - ideally, logic to determine if subscription (like ``Generic[T, DefaultT]``) would be valid. -A reference implementation of the above can be found at +A reference implementation of the type checker can be found at https://github.com/Gobot1234/mypy/tree/TypeVar-defaults Interaction with PEP 695 @@ -267,7 +266,7 @@ Interaction with PEP 695 If this PEP were to be accepted, amendments to PEP 695 could be made to allow for specifying defaults for type parameters using the new syntax. -Specifying a default should be done using the “=” operator inside of the +Specifying a default should be done using the "=" operator inside of the square brackets like so: .. code:: py From a8c7c070e031d3d44f5d5201535751b9ad3177d5 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sat, 16 Jul 2022 14:24:38 +0100 Subject: [PATCH 04/13] Take 696 --- .github/CODEOWNERS | 2 +- pep-9999.rst => pep-0696.rst | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pep-9999.rst => pep-0696.rst (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index efb92d0d348..13b34db2b26 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -576,7 +576,7 @@ pep-0692.rst @jellezijlstra pep-0693.rst @Yhg1s pep-0694.rst @dstufft pep-0695.rst @gvanrossum -pep-9999.rst @jellezijlstra +pep-0696.rst @jellezijlstra # ... # pep-0754.txt # ... diff --git a/pep-9999.rst b/pep-0696.rst similarity index 100% rename from pep-9999.rst rename to pep-0696.rst From 1f31faee204d01ff8204bd38294cf88d7a03b983 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sat, 16 Jul 2022 14:37:07 +0100 Subject: [PATCH 05/13] Respond to initial review Co-authored-by: Jelle Zijlstra --- pep-0696.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index ff20a0aec71..d3c831e05a4 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -65,7 +65,7 @@ This is also useful for a Generic that is commonly over one type. reveal_type(Context().bot) # type is Bot # notice this is not Any which is what it would be currently reveal_type(Context[MyBot]().bot) # type is MyBot -Not only does this improve typing for those who explicitly use it. It +Not only does this improve typing for those who explicitly use it, it also helps non-typing users who rely on auto-complete to speed up their development. @@ -80,8 +80,8 @@ This design pattern is common in projects like: useful to simplify the definition of ``Layer``) `This proposal could also be used on builtins.slice `__ -where the parameter should ``start`` default to ``int``, ``stop`` -default to ``start`` and step default to ``int | None`` +where the ``start`` parameter should default to ``int``, ``stop`` +default to the type of ``start`` and step default to ``int | None`` .. code:: py @@ -138,7 +138,7 @@ should flag this an error. AllTheDefaults[int, complex, str, int, bool] ) # All valid -This cannot be enforce at runtime for functions, for now, but in the +This cannot be enforced at runtime for functions, for now, but in the future, this might be possible (see `Interaction with PEP 695 <#interaction-with-pep-695>`__). @@ -189,7 +189,8 @@ Using bound and default ~~~~~~~~~~~~~~~~~~~~~~~ If both ``bound`` and ``default`` are passed ``default`` must be a -subtype of ``bound``. +subtype of ``bound`` otherwise the type checker should generate an +error. .. code:: py @@ -264,7 +265,7 @@ https://github.com/Gobot1234/mypy/tree/TypeVar-defaults Interaction with PEP 695 ------------------------ -If this PEP were to be accepted, amendments to PEP 695 could be made to +If this PEP were to be accepted, amendments to :pep:`695` could be made to allow for specifying defaults for type parameters using the new syntax. Specifying a default should be done using the "=" operator inside of the square brackets like so: @@ -365,7 +366,7 @@ break a lot of codebases. This is also solvable in most cases using a Having ``default`` implicitly be ``bound`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In an earlier version of this PEP, the ``default`` was implicitly the +In an earlier version of this PEP, the ``default`` was implicitly set to ``bound`` if no value was passed for ``default``. This while convenient, could have a ``TypeVar`` with no default follow a ``TypeVar`` with a default. Consider: @@ -386,8 +387,10 @@ convenient, could have a ``TypeVar`` with no default follow a class Foo(Generic[T, U]): ... +Acknowledgements +---------------- Thanks to the following people for their feedback on the PEP: Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morgan -and Jakub Kuczys \ No newline at end of file +and Jakub Kuczys From 1b037e9c409a84341233efc86b061efd7ba18abb Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sat, 16 Jul 2022 14:46:44 +0100 Subject: [PATCH 06/13] Forgot to update the actual pep number --- pep-0696.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pep-0696.rst b/pep-0696.rst index d3c831e05a4..c2b5e8cf82d 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -1,4 +1,4 @@ -PEP: 9999 +PEP: 696 Title: Type defaults for TypeVars Version: $Revision$ Last-Modified: $Date$ From a0195a9225f3cd0aed6f5ba6ce1d1d9163722b89 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sun, 17 Jul 2022 00:57:31 +0100 Subject: [PATCH 07/13] Respond to Eric's comments --- pep-0696.rst | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index c2b5e8cf82d..ea8f950c5c2 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -21,6 +21,12 @@ Abstract This PEP introduces the concept of type defaults for TypeVars, which act as defaults for a type parameter when none is specified. +Default type argument support is available in some popular languages +such as C++, TypeScript, and Rust. A survey of type parameter syntax in +some common languages has been conducted by the author of :pep:`695` +and can be found in its +:pep:`Appendix A <695#appendix-a-survey-of-type-parameter-syntax>`. + Motivation ---------- @@ -201,8 +207,8 @@ Constraints ~~~~~~~~~~~ For constrained ``TypeVar``\ s, the default needs to be one of the -constraints. It would be an error even if it is a subtype of one of the -constraints. +constraints. A type checker should generate an error even if it is a +subtype of one of the constraints. .. code:: py @@ -213,8 +219,24 @@ Function Defaults ~~~~~~~~~~~~~~~~~ The ``TypeVar``\ 's default should also be compatible with the -parameter's runtime default if present. But they are erased to -not have defaults when called. +parameter's runtime default if present, if they aren't a type checker +should generate an error. + +.. code:: py + + def foo(x: DefaultIntT = 12345): ... # Valid + def foo(x: DefaultIntT = "bar"): ... # Invalid: expected an int for default, got str + +The defaults are erased when called so, they act like they have no default. + +.. code:: py + + def baz(y: DefaultIntT) -> DefaultIntT: ... + # equivalent to + def baz(y: T) -> T: ... + + reveal_type(bar(67890)) # type is int + reveal_type(bar("hello")) # type is str Implementation -------------- @@ -387,6 +409,9 @@ convenient, could have a ``TypeVar`` with no default follow a class Foo(Generic[T, U]): ... +This would have also been a breaking change for a small number of cases +where the code relied on ``Any`` being the implicit default. + Acknowledgements ---------------- From f104c168fa0a3460da78dfde058aa0d592d6de14 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Mon, 18 Jul 2022 17:42:32 +0100 Subject: [PATCH 08/13] Fix formatting nits Co-authored-by: Hugo van Kemenade --- pep-0696.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index ea8f950c5c2..f09f517b334 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -12,9 +12,6 @@ Content-Type: text/x-rst Created: 14-Jul-2022 Python-Version: 3.12 -Type defaults for TypeVars -========================== - Abstract -------- @@ -55,7 +52,7 @@ propose changing the *stub definition* to something like: Generator[int] == Generator[int, None] == Generator[int, None, None] -This is also useful for a Generic that is commonly over one type. +This is also useful for a ``Generic`` that is commonly over one type. .. code:: py @@ -79,11 +76,11 @@ This design pattern is common in projects like: - `discord.py `__ - where the example above was taken from. - `NumPy `__ - the default for types - like ``ndarray``\ 's ``dtype`` would be ``float64``. Currently it's + like ``ndarray``'s ``dtype`` would be ``float64``. Currently it's ``Unknown`` or ``Any``. - `TensorFlow `__ (this could be used for Tensor similarly to ``numpy.ndarray`` and would be - useful to simplify the definition of ``Layer``) + useful to simplify the definition of ``Layer``). `This proposal could also be used on builtins.slice `__ where the ``start`` parameter should default to ``int``, ``stop`` @@ -218,7 +215,7 @@ subtype of one of the constraints. Function Defaults ~~~~~~~~~~~~~~~~~ -The ``TypeVar``\ 's default should also be compatible with the +The ``TypeVar``'s default should also be compatible with the parameter's runtime default if present, if they aren't a type checker should generate an error. @@ -331,7 +328,7 @@ An older version of this PEP included a specification for removed as few practical use cases for the two were found. Maybe this can be revisited. -Allowing the ``TypeVar`` defaults to be passed to ``type.__new__``\ 's ``**kwargs`` +Allowing the ``TypeVar`` defaults to be passed to ``type.__new__``'s ``**kwargs`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: py From 33b74c794c86d6b7c7a1a022057d4c5c0c4cbb7e Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 19 Jul 2022 15:22:16 +0100 Subject: [PATCH 09/13] Widen to support all TypeVarLikes --- pep-0696.rst | 183 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 113 insertions(+), 70 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index f09f517b334..a99366a8fd2 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -1,5 +1,5 @@ PEP: 696 -Title: Type defaults for TypeVars +Title: Type defaults for TypeVarLikes Version: $Revision$ Last-Modified: $Date$ Author: James Hilton-Balfe @@ -15,8 +15,9 @@ Python-Version: 3.12 Abstract -------- -This PEP introduces the concept of type defaults for TypeVars, which act -as defaults for a type parameter when none is specified. +This PEP introduces the concept of type defaults for +``TypeVarLike``\ s (``TypeVar``, ``ParamSpec`` and ``TypeVarTuple``), +which act as defaults for a type parameter when none is specified. Default type argument support is available in some popular languages such as C++, TypeScript, and Rust. A survey of type parameter syntax in @@ -82,18 +83,6 @@ This design pattern is common in projects like: could be used for Tensor similarly to ``numpy.ndarray`` and would be useful to simplify the definition of ``Layer``). -`This proposal could also be used on builtins.slice `__ -where the ``start`` parameter should default to ``int``, ``stop`` -default to the type of ``start`` and step default to ``int | None`` - -.. code:: py - - StartT = TypeVar("StartT", default=int) - StopT = TypeVar("StopT", default=StartT) - StepT = TypeVar("StepT", default=int | None) - - class slice(Generic[StartT, StopT, StepT]): ... - Specification ------------- @@ -101,8 +90,8 @@ Default ordering and subscription rules ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The order for defaults should follow the standard function parameter -rules, so a ``TypeVar`` with no ``default`` cannot follow one with a -``default`` value. Doing so should ideally raise a ``TypeError`` in +rules, so a ``TypeVarLike`` with no ``default`` cannot follow one with +a ``default`` value. Doing so should ideally raise a ``TypeError`` in ``typing._GenericAlias``/``types.GenericAlias``, and a type checker should flag this an error. @@ -145,11 +134,72 @@ This cannot be enforced at runtime for functions, for now, but in the future, this might be possible (see `Interaction with PEP 695 <#interaction-with-pep-695>`__). +``ParamSpec`` Defaults +~~~~~~~~~~~~~~~~~~~~~~ + +``ParamSpec`` defaults are defined using the same syntax as +``TypeVar`` \ s but uses a ``list`` or ``tuple`` of types instead of a +single type. + +.. code:: py + + DefaultP = ParamSpec("DefaultP", default=(str, int)) + + class Foo(Generic[DefaultP]): ... + + reveal_type(Foo()) # type is Foo[(str, int)] + reveal_type(Foo[(bool, bool)]()) # type is Foo[(bool, bool)] + +``TypeVarTuple`` Defaults +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``TypeVarTuple`` defaults are defined using the same syntax as +``TypeVar`` \ s but uses an unpacked tuple of types instead of a single type. + +.. code:: py + + DefaultTs = TypeVarTuple("DefaultTs", default=*tuple[str, int]) + + class Foo(Generic[DefaultTs]): ... + + reveal_type(Foo()) # type is Foo[str, int] + reveal_type(Foo[int, bool]()) # type is Foo[int, bool] + +Using another ``TypeVarLike`` as the default +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To use another ``TypeVarLike``\ s as the default they have to be of the +same type. When using another ``TypeVarLike`` (T1) as the default, the default +for the ``TypeVarLike`` (T2), T1 must be used before in the signature +of the class it appears in before T2. T2's bound must be a subtype of +T1's bound. + +`This could be used on builtins.slice `__ +where the ``start`` parameter should default to ``int``, ``stop`` +default to the type of ``start`` and step default to ``int | None``. + +.. code:: py + + StartT = TypeVar("StartT", default=int) + StopT = TypeVar("StopT", default=StartT) + StepT = TypeVar("StepT", default=int | None) + + class slice(Generic[StartT, StopT, StepT]): ... # Valid + + reveal_type(slice()) # type is slice[int, int, int | None] + reveal_type(slice[str]()) # type is slice[str, str, int | None] + reveal_type(slice[str, str, timedelta]()) # type is slice[str, str, timedelta] + + StartT = TypeVar("StartT", default="StopT") + StopT = TypeVar("StopT", default=int) + class slice(Generic[StartT, StopT, StepT]): ... + ^^^^^^ # Invalid: ordering does not allow StopT to bound yet + ``Generic`` ``TypeAlias``\ es ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``Generic`` ``TypeAlias``\ es should be able to be further subscripted -following normal subscription rules. If a ``TypeVar`` has a default +following normal subscription rules. If a ``TypeVarLike`` has a default that hasn't been overridden it should be treated like it was substituted into the ``TypeAlias``. However, it can be specialised further down the line. @@ -159,15 +209,15 @@ further down the line. class SomethingWithNoDefaults(Generic[T, T2]): ... MyAlias: TypeAlias = SomethingWithNoDefaults[int, DefaultStrT] # valid - reveal_type(MyAlias) # type is SomethingWithNoDefaults[int, str] - reveal_type(MyAlias[bool]) # type is SomethingWithNoDefaults[int, bool] + reveal_type(MyAlias()) # type is SomethingWithNoDefaults[int, str] + reveal_type(MyAlias[bool]()) # type is SomethingWithNoDefaults[int, bool] MyAlias[bool, int] # Invalid: too many arguments passed to MyAlias Subclassing ~~~~~~~~~~~ -Subclasses of ``Generic``\ s with ``TypeVar``\ s that have defaults +Subclasses of ``Generic``\ s with ``TypeVarLike``\ s that have defaults behave similarly to ``Generic`` ``TypeAlias``\ es. .. code:: py @@ -175,18 +225,19 @@ behave similarly to ``Generic`` ``TypeAlias``\ es. class SubclassMe(Generic[T, DefaultStrT]): ... class Bar(SubclassMe[int, DefaultStrT]): ... - reveal_type(Bar) # type is Bar[str] - reveal_type(Bar[bool]) # type is Bar[bool] + reveal_type(Bar()) # type is Bar[str] + reveal_type(Bar[bool]()) # type is Bar[bool] class Foo(SubclassMe[int]): ... - reveal_type(Spam) # type is + reveal_type(Foo()) # type is + Foo[str] # Invalid: Foo cannot be further subscripted class Baz(Generic[DefaultIntT, DefaultStrT]): ... class Spam(Baz): ... - reveal_type(Spam) # type is + reveal_type(Spam()) # type is Using bound and default ~~~~~~~~~~~~~~~~~~~~~~~ @@ -215,41 +266,36 @@ subtype of one of the constraints. Function Defaults ~~~~~~~~~~~~~~~~~ -The ``TypeVar``'s default should also be compatible with the -parameter's runtime default if present, if they aren't a type checker -should generate an error. +``TypeVarLike``\ s currently can only be used where a parameter can go unsolved. .. code:: py - def foo(x: DefaultIntT = 12345): ... # Valid - def foo(x: DefaultIntT = "bar"): ... # Invalid: expected an int for default, got str - -The defaults are erased when called so, they act like they have no default. - -.. code:: py + def foo(a: DefaultStrT | None = None) -> DefaultStrT: ... - def baz(y: DefaultIntT) -> DefaultIntT: ... - # equivalent to - def baz(y: T) -> T: ... + reveal_type(foo(1)) # type is int + reveal_type(foo()) # type is str - reveal_type(bar(67890)) # type is int - reveal_type(bar("hello")) # type is str +If they are used where the parameter type is known, the defaults +should just be ignored and a type checker can emit a warning. Implementation -------------- -At runtime, this would involve the following changes to -``typing.TypeVar``: +At runtime, this would involve the following changes to the ``typing`` +module. -- the type passed to default would be available as a ``__default__`` - attribute. +- a new class ``_DefaultMixin`` needs to be added which is a superclass + of ``TypeVar``, ``ParamSpec``, and ``TypeVarTuple``. + + - the type passed to default would be available as a ``__default__`` + attribute. The following changes would be required to both ``GenericAlias``\ es: - logic to determine the defaults required for a subscription. - potentially a way construct ``types.GenericAliases`` using a - classmethod to allow for defaults in + ``classmethod`` to allow for defaults in ``__class_getitem__ = classmethod(GenericAlias)`` i.e. ``GenericAlias.with_type_var_likes()``. @@ -291,45 +337,42 @@ square brackets like so: .. code:: py + # TypeVars class Foo[T = str]: ... - def bar[U = int](): ... -This functionality was included in the initial draft of PEP 695 but was -removed due to scope creep. + # ParamSpecs + class Baz[**P = (int, str)]: ... + def spam[**Q = (bool,)](): ... + + # TypeVarTuples + class Qux[*Ts = *tuple[int, bool]]: ... + def ham[*Us = *tuple[str]](): ... + +This functionality was included in the initial draft of :pep:`695` but +was removed due to scope creep. Grammar Changes ~~~~~~~~~~~~~~~ :: - type_param: - | a=NAME b=[type_param_bound] d=[type_param_default] - | a=NAME c=[type_param_constraint] d=[type_param_default] - | '*' a=NAME d=[type_param_default] - | '**' a=NAME d=[type_param_default] + type_param: + | a=NAME b=[type_param_bound] d=[type_param_default] + | a=NAME c=[type_param_constraint] d=[type_param_default] + | '*' a=NAME d=[type_param_default] + | '**' a=NAME d=[type_param_default] - type_param_default: '=' e=expression + type_param_default: '=' e=expression -This would mean that ``TypeVarLikes`` with defaults proceeding those -with non-defaults can be checked at compile time. Although this version -of the PEP does not define behaviour for ``TypeVarTuple`` and -``ParamSpec`` defaults, this would mean they can be added easily in the -future. +This would mean that ``TypeVarLike``\ s with defaults proceeding those +with non-defaults can be checked at compile time. Rejected Alternatives --------------------- -Specification for ``TypeVarTuple`` and ``ParamSpec`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An older version of this PEP included a specification for -``TypeVarTuple`` and ``ParamSpec`` defaults. However, this has been -removed as few practical use cases for the two were found. Maybe this -can be revisited. - -Allowing the ``TypeVar`` defaults to be passed to ``type.__new__``'s ``**kwargs`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allowing the ``TypeVarLike``\ s defaults to be passed to ``type.__new__``'s ``**kwargs`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: py @@ -387,8 +430,8 @@ Having ``default`` implicitly be ``bound`` In an earlier version of this PEP, the ``default`` was implicitly set to ``bound`` if no value was passed for ``default``. This while -convenient, could have a ``TypeVar`` with no default follow a -``TypeVar`` with a default. Consider: +convenient, could have a ``TypeVarLike`` with no default follow a +``TypeVarLike`` with a default. Consider: .. code:: py From 7f02de57b9e18cc1f3f43164d3daf6cc32851041 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 19 Jul 2022 22:22:24 +0100 Subject: [PATCH 10/13] Remove unnecessary header items Co-authored-by: Hugo van Kemenade --- pep-0696.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index a99366a8fd2..a6cba709d08 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -1,7 +1,5 @@ PEP: 696 Title: Type defaults for TypeVarLikes -Version: $Revision$ -Last-Modified: $Date$ Author: James Hilton-Balfe Sponsor: Jelle Zijlstra Discussions-To: typing-sig@python.org From 8df02a5ff50de001436bdc511c2110633d794351 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Wed, 20 Jul 2022 21:52:48 +0100 Subject: [PATCH 11/13] Hopefully final changes --- pep-0696.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index a6cba709d08..1aea1c1271e 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -136,8 +136,8 @@ future, this might be possible (see `Interaction with PEP ~~~~~~~~~~~~~~~~~~~~~~ ``ParamSpec`` defaults are defined using the same syntax as -``TypeVar`` \ s but uses a ``list`` or ``tuple`` of types instead of a -single type. +``TypeVar`` \ s but use a ``list`` or ``tuple`` of types or an ellipsis +literal "``...``". .. code:: py @@ -152,11 +152,11 @@ single type. ~~~~~~~~~~~~~~~~~~~~~~~~~ ``TypeVarTuple`` defaults are defined using the same syntax as -``TypeVar`` \ s but uses an unpacked tuple of types instead of a single type. +``TypeVar`` \ s but use an unpacked tuple of types instead of a single type. .. code:: py - DefaultTs = TypeVarTuple("DefaultTs", default=*tuple[str, int]) + DefaultTs = TypeVarTuple("DefaultTs", default=Unpack[tuple[str, int]]) class Foo(Generic[DefaultTs]): ... @@ -241,7 +241,7 @@ Using bound and default ~~~~~~~~~~~~~~~~~~~~~~~ If both ``bound`` and ``default`` are passed ``default`` must be a -subtype of ``bound`` otherwise the type checker should generate an +subtype of ``bound``. Otherwise the type checker should generate an error. .. code:: py @@ -328,10 +328,9 @@ https://github.com/Gobot1234/mypy/tree/TypeVar-defaults Interaction with PEP 695 ------------------------ -If this PEP were to be accepted, amendments to :pep:`695` could be made to -allow for specifying defaults for type parameters using the new syntax. -Specifying a default should be done using the "=" operator inside of the -square brackets like so: +If this PEP is accepted, the syntax proposed in :pep:`695` will be +extended to introduce a way to specify defaults for type parameters +using the "=" operator inside of the square brackets like so: .. code:: py From b94ba592e30b211797e87579dde47b1b1e560e2d Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Fri, 22 Jul 2022 09:49:54 +0100 Subject: [PATCH 12/13] Some style nits + add copyright --- pep-0696.rst | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index 1aea1c1271e..6f03758da25 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -23,6 +23,7 @@ some common languages has been conducted by the author of :pep:`695` and can be found in its :pep:`Appendix A <695#appendix-a-survey-of-type-parameter-syntax>`. + Motivation ---------- @@ -81,11 +82,12 @@ This design pattern is common in projects like: could be used for Tensor similarly to ``numpy.ndarray`` and would be useful to simplify the definition of ``Layer``). + Specification ------------- Default ordering and subscription rules -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +''''''''''''''''''''''''''''''''''''''' The order for defaults should follow the standard function parameter rules, so a ``TypeVarLike`` with no ``default`` cannot follow one with @@ -133,7 +135,7 @@ future, this might be possible (see `Interaction with PEP 695 <#interaction-with-pep-695>`__). ``ParamSpec`` Defaults -~~~~~~~~~~~~~~~~~~~~~~ +'''''''''''''''''''''' ``ParamSpec`` defaults are defined using the same syntax as ``TypeVar`` \ s but use a ``list`` or ``tuple`` of types or an ellipsis @@ -149,7 +151,7 @@ literal "``...``". reveal_type(Foo[(bool, bool)]()) # type is Foo[(bool, bool)] ``TypeVarTuple`` Defaults -~~~~~~~~~~~~~~~~~~~~~~~~~ +''''''''''''''''''''''''' ``TypeVarTuple`` defaults are defined using the same syntax as ``TypeVar`` \ s but use an unpacked tuple of types instead of a single type. @@ -164,7 +166,7 @@ literal "``...``". reveal_type(Foo[int, bool]()) # type is Foo[int, bool] Using another ``TypeVarLike`` as the default -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'''''''''''''''''''''''''''''''''''''''''''' To use another ``TypeVarLike``\ s as the default they have to be of the same type. When using another ``TypeVarLike`` (T1) as the default, the default @@ -194,7 +196,7 @@ default to the type of ``start`` and step default to ``int | None``. ^^^^^^ # Invalid: ordering does not allow StopT to bound yet ``Generic`` ``TypeAlias``\ es -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +''''''''''''''''''''''''''''' ``Generic`` ``TypeAlias``\ es should be able to be further subscripted following normal subscription rules. If a ``TypeVarLike`` has a default @@ -213,7 +215,7 @@ further down the line. MyAlias[bool, int] # Invalid: too many arguments passed to MyAlias Subclassing -~~~~~~~~~~~ +''''''''''' Subclasses of ``Generic``\ s with ``TypeVarLike``\ s that have defaults behave similarly to ``Generic`` ``TypeAlias``\ es. @@ -238,7 +240,7 @@ behave similarly to ``Generic`` ``TypeAlias``\ es. reveal_type(Spam()) # type is Using bound and default -~~~~~~~~~~~~~~~~~~~~~~~ +''''''''''''''''''''''' If both ``bound`` and ``default`` are passed ``default`` must be a subtype of ``bound``. Otherwise the type checker should generate an @@ -250,7 +252,7 @@ error. TypeVar("Invalid", bound=str, default=int) # Invalid: the bound and default are incompatible Constraints -~~~~~~~~~~~ +''''''''''' For constrained ``TypeVar``\ s, the default needs to be one of the constraints. A type checker should generate an error even if it is a @@ -262,7 +264,7 @@ subtype of one of the constraints. TypeVar("Invalid", float, str, default=int) # Invalid: expected one of float or str got int Function Defaults -~~~~~~~~~~~~~~~~~ +''''''''''''''''' ``TypeVarLike``\ s currently can only be used where a parameter can go unsolved. @@ -276,6 +278,7 @@ Function Defaults If they are used where the parameter type is known, the defaults should just be ignored and a type checker can emit a warning. + Implementation -------------- @@ -325,6 +328,7 @@ The following changes would be required to both ``GenericAlias``\ es: A reference implementation of the type checker can be found at https://github.com/Gobot1234/mypy/tree/TypeVar-defaults + Interaction with PEP 695 ------------------------ @@ -350,7 +354,7 @@ This functionality was included in the initial draft of :pep:`695` but was removed due to scope creep. Grammar Changes -~~~~~~~~~~~~~~~ +''''''''''''''' :: @@ -365,11 +369,12 @@ Grammar Changes This would mean that ``TypeVarLike``\ s with defaults proceeding those with non-defaults can be checked at compile time. + Rejected Alternatives --------------------- Allowing the ``TypeVarLike``\ s defaults to be passed to ``type.__new__``'s ``**kwargs`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' .. code:: py @@ -397,7 +402,7 @@ Ideally, if :pep:`637` wasn't rejected, the following would be acceptable: value: T | None = None Allowing non-defaults to follow defaults -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'''''''''''''''''''''''''''''''''''''''' .. code:: py @@ -423,7 +428,7 @@ break a lot of codebases. This is also solvable in most cases using a Coro[int] == Coroutine[Any, Any, int] Having ``default`` implicitly be ``bound`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'''''''''''''''''''''''''''''''''''''''''' In an earlier version of this PEP, the ``default`` was implicitly set to ``bound`` if no value was passed for ``default``. This while @@ -449,6 +454,7 @@ convenient, could have a ``TypeVarLike`` with no default follow a This would have also been a breaking change for a small number of cases where the code relied on ``Any`` being the implicit default. + Acknowledgements ---------------- @@ -456,3 +462,9 @@ Thanks to the following people for their feedback on the PEP: Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morgan and Jakub Kuczys + + +Copyright +--------- +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From 63f4816f211b1a0541e6136cc0778bb8d23d445c Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Sun, 24 Jul 2022 16:56:43 +0100 Subject: [PATCH 13/13] Remove GenericAlias.with_typevar_likes --- pep-0696.rst | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/pep-0696.rst b/pep-0696.rst index 6f03758da25..06bb9317296 100644 --- a/pep-0696.rst +++ b/pep-0696.rst @@ -294,34 +294,6 @@ module. The following changes would be required to both ``GenericAlias``\ es: - logic to determine the defaults required for a subscription. - - - potentially a way construct ``types.GenericAliases`` using a - ``classmethod`` to allow for defaults in - ``__class_getitem__ = classmethod(GenericAlias)`` - i.e. ``GenericAlias.with_type_var_likes()``. - - .. code:: py - - # _collections_abc.py - - _sentinel = object() - - # NOTE: this is not actually typing.TypeVar, that's in typing.py, - # this is just to trick is_typevar() in genericaliasobject.c - class TypeVar: - __module__ = "typing" - - def __init__(self, name, *, default=_sentinel): - self.__name__ = name - self.__default__ = default - - YieldT = TypeVar("YieldT") - SendT = TypeVar("SendT", default=None) - ReturnT = TypeVar("ReturnT", default=None) - - class Generator(Iterable): - __class_getitem__ = GenericAlias.with_type_var_likes(YieldT, SendT, ReturnT) - - ideally, logic to determine if subscription (like ``Generic[T, DefaultT]``) would be valid.