From 5d3c1f82772b7414e8e41bd0b1d76b2fda2760c1 Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Thu, 31 Oct 2024 15:22:06 -0700 Subject: [PATCH 1/8] Update generics.rst --- docs/spec/generics.rst | 65 ++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index 2cb3f6f6..ec15f5cb 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -93,8 +93,9 @@ This is equivalent to omitting the generic notation and just saying User-defined generic types -------------------------- -You can include a ``Generic`` base class to define a user-defined class -as generic. Example:: +You can define a user-defined class as generic by including a ``Generic`` +base class, either explicitly or implicitly via the ``Protocol[T]`` +shorthand for :ref:`generic protocols`. Example:: from typing import TypeVar, Generic from logging import Logger @@ -144,7 +145,6 @@ A generic type can have any number of type variables, and type variables may be constrained. This is valid:: from typing import TypeVar, Generic - ... T = TypeVar('T') S = TypeVar('S') @@ -156,29 +156,16 @@ Each type variable argument to ``Generic`` must be distinct. This is thus invalid:: from typing import TypeVar, Generic - ... T = TypeVar('T') class Pair(Generic[T, T]): # INVALID ... -The ``Generic[T]`` base class is redundant in simple cases where you -subclass some other generic class and specify type variables for its -parameters:: - - from typing import TypeVar - from collections.abc import Iterator - - T = TypeVar('T') - - class MyIter(Iterator[T]): - ... - -That class definition is equivalent to:: - - class MyIter(Iterator[T], Generic[T]): - ... +When no ``Generic[T]`` or ``Protocol[T]`` base class is present, a defined +class is generic if you subclass one or more other generic classes and +specify type variables for their parameters. See :ref:`generic-base-classes` +for details. You can use multiple inheritance with ``Generic``:: @@ -402,6 +389,7 @@ instead is preferred. (First, creating the subscripted class, e.g. ``Node[int]``, has a runtime cost. Second, using a type alias is more readable.) +.. _`generic-base-classes`: Arbitrary generic types as base classes --------------------------------------- @@ -458,8 +446,43 @@ Also consider the following example:: class MyDict(Mapping[str, T]): ... -In this case MyDict has a single parameter, T. +In this case ``MyDict`` has a single parameter, ``T``. + +Type variables are applied to the defined class in the order in which +they first appear in any generic base classes:: + from typing import Generic, TypeVar + + T1 = TypeVar('T1') + T2 = TypeVar('T2') + T3 = TypeVar('T3') + + class Parent1(Generic[T1, T2]): + ... + class Parent2(Generic[T1, T2]): + ... + class Child(Parent1[T1, T3], Parent2[T2, T3]): + ... + +That ``Child`` definition is equivalent to:: + + class Child(Parent1[T1, T3], Parent2[T2, T3], Generic[T1, T3, T2]): + ... + +Type checkers may warn when the type variable order is inconsistent:: + + from typing import Generic, TypeVar + + T1 = TypeVar('T2') + T2 = TypeVar('T2') + T3 = TypeVar('T3') + + class Grandparent(Generic[T1, T2]): + ... + class Parent(Grandparent[T1, T2]): + ... + class Child(Parent[T1, T2], Grandparent[T2, T1]): # Inconsistent order + ... Abstract generic types ---------------------- From 599d41a9df35eee68a18a68b0455c540ffbb5590 Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Thu, 31 Oct 2024 15:25:04 -0700 Subject: [PATCH 2/8] Update protocol.rst --- docs/spec/protocol.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/spec/protocol.rst b/docs/spec/protocol.rst index e280f0ed..e9d3a428 100644 --- a/docs/spec/protocol.rst +++ b/docs/spec/protocol.rst @@ -257,6 +257,7 @@ from regular ABCs, where abstractness is simply defined by having at least one abstract method being unimplemented. Protocol classes must be marked *explicitly*. +.. _`generic-protocols`: Generic protocols ^^^^^^^^^^^^^^^^^ @@ -271,7 +272,8 @@ non-protocol generic types:: ... ``Protocol[T, S, ...]`` is allowed as a shorthand for -``Protocol, Generic[T, S, ...]``. +``Protocol, Generic[T, S, ...]``. It is an error to include both +``Protocol[T, S, ...]`` and ``Generic[T, S, ...]`` in a class definition. User-defined generic protocols support explicitly declared variance. Type checkers will warn if the inferred variance is different from From 1405f01e4af779eb5418743ff546ebba17a80cd5 Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Thu, 31 Oct 2024 19:18:27 -0700 Subject: [PATCH 3/8] Address reviewer comments. --- docs/spec/generics.rst | 16 ++++++++++------ docs/spec/protocol.rst | 11 +++++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index ec15f5cb..892a2590 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -93,9 +93,13 @@ This is equivalent to omitting the generic notation and just saying User-defined generic types -------------------------- -You can define a user-defined class as generic by including a ``Generic`` -base class, either explicitly or implicitly via the ``Protocol[T]`` -shorthand for :ref:`generic protocols`. Example:: +You can define a user-defined class as generic in three ways: by including a +``Generic`` base class, by using the new generic class syntax in Python 3.12 +and higher, or by including a ``Protocol`` base class parameterized with type +variables. The third approach also marks the class as a protocol - see +:ref:`generic protocols` for more information. + +Example using ``Generic``:: from typing import TypeVar, Generic from logging import Logger @@ -119,14 +123,14 @@ shorthand for :ref:`generic protocols`. Example:: def log(self, message: str) -> None: self.logger.info('{}: {}'.format(self.name, message)) -Or, in Python 3.12 and higher, by using the new syntax for generic -classes:: +Or, using the new generic class syntax:: class LoggedVar[T]: # methods as in previous example This implicitly adds ``Generic[T]`` as a base class and type checkers -should treat the two largely equivalently (except for variance, see below). +should treat the two definitions of ``LoggedVar`` largely equivalently (except +for variance, see below). ``Generic[T]`` as a base class defines that the class ``LoggedVar`` takes a single type parameter ``T``. This also makes ``T`` valid as diff --git a/docs/spec/protocol.rst b/docs/spec/protocol.rst index e9d3a428..50b64f37 100644 --- a/docs/spec/protocol.rst +++ b/docs/spec/protocol.rst @@ -272,8 +272,15 @@ non-protocol generic types:: ... ``Protocol[T, S, ...]`` is allowed as a shorthand for -``Protocol, Generic[T, S, ...]``. It is an error to include both -``Protocol[T, S, ...]`` and ``Generic[T, S, ...]`` in a class definition. +``Protocol, Generic[T, S, ...]``. It is an error to combine +``Protocol[T, S, ...]`` with ``Generic[T, S, ...]``, or with the new syntax for +generic classes in Python 3.12 and above:: + + class Iterable(Protocol[T], Generic[T]): # INVALID + ... + + class Iterable[T](Protocol[T]): # INVALID + ... User-defined generic protocols support explicitly declared variance. Type checkers will warn if the inferred variance is different from From 5b2024737e65b778d438108d187e1252bb87c3ba Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Thu, 31 Oct 2024 19:29:35 -0700 Subject: [PATCH 4/8] Fix silly typo --- docs/spec/generics.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index 892a2590..da807bf9 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -477,7 +477,7 @@ Type checkers may warn when the type variable order is inconsistent:: from typing import Generic, TypeVar - T1 = TypeVar('T2') + T1 = TypeVar('T1') T2 = TypeVar('T2') T3 = TypeVar('T3') From 59198a1fef63d554e8248756f963196fe4b050b0 Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Fri, 1 Nov 2024 10:59:01 -0700 Subject: [PATCH 5/8] English --- docs/spec/generics.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index da807bf9..c47d84ba 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -128,7 +128,7 @@ Or, using the new generic class syntax:: class LoggedVar[T]: # methods as in previous example -This implicitly adds ``Generic[T]`` as a base class and type checkers +This implicitly adds ``Generic[T]`` as a base class, and type checkers should treat the two definitions of ``LoggedVar`` largely equivalently (except for variance, see below). @@ -450,7 +450,7 @@ Also consider the following example:: class MyDict(Mapping[str, T]): ... -In this case ``MyDict`` has a single parameter, ``T``. +In this case ``MyDict`` has a single type parameter, ``T``. Type variables are applied to the defined class in the order in which they first appear in any generic base classes:: From 6ac31968f98c1f1319b987831b432a238fb3395f Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Fri, 1 Nov 2024 12:51:55 -0700 Subject: [PATCH 6/8] Incorporate additional rules suggested by @erictraut. --- docs/spec/generics.rst | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index c47d84ba..ecfc209b 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -166,8 +166,44 @@ thus invalid:: class Pair(Generic[T, T]): # INVALID ... -When no ``Generic[T]`` or ``Protocol[T]`` base class is present, a defined -class is generic if you subclass one or more other generic classes and +All arguments to ``Generic`` or ``Protocol`` must be type variables:: + + from typing import Generic, Protocol + + class Bad1(Generic[int]): # INVALID + ... + class Bad2(Protocol[int]): # INVALID + ... + +When a ``Generic`` or parameterized ``Protocol`` base class is present, all type +parameters for the class must appear within the ``Generic`` or +``Protocol`` type argument list, respectively. A type checker should report an +error if a type variable that is not included in the type argument list appears +elsewhere in the base class list:: + + from typing import Generic, Protocol, TypeVar + from collections.abc import Iterable + + T = TypeVar('T') + S = TypeVar('S') + + class Bad1(Iterable[T], Generic[S]): # INVALID + ... + class Bad2(Iterable[T], Protocol[S]): # INVALID + ... + +Note that the above rule does not apply to a bare ``Protocol`` base class. This +is valid (see below):: + + from typing import Protocol, TypeVar + from collections.abc import Iterator + + T = TypeVar('T') + + class MyIterator(Iterator[T], Protocol): ... + +When no ``Generic`` or parameterized ``Protocol`` base class is present, a +defined class is generic if you subclass one or more other generic classes and specify type variables for their parameters. See :ref:`generic-base-classes` for details. From 057f9bfa38affd7fc6a4b109a27c82140d3cd96a Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Sat, 2 Nov 2024 12:03:28 -0700 Subject: [PATCH 7/8] Mandate error on inconsistent type variable order. --- docs/spec/generics.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index ecfc209b..7ad83384 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -509,7 +509,8 @@ That ``Child`` definition is equivalent to:: class Child(Parent1[T1, T3], Parent2[T2, T3], Generic[T1, T3, T2]): ... -Type checkers may warn when the type variable order is inconsistent:: +A type checker should report an error when the type variable order is +inconsistent:: from typing import Generic, TypeVar @@ -521,7 +522,7 @@ Type checkers may warn when the type variable order is inconsistent:: ... class Parent(Grandparent[T1, T2]): ... - class Child(Parent[T1, T2], Grandparent[T2, T1]): # Inconsistent order + class Child(Parent[T1, T2], Grandparent[T2, T1]): # INVALID ... Abstract generic types From 8d7c1d93eae32b16eedd9258e04a1489bde226cb Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Wed, 20 Nov 2024 13:24:49 -0800 Subject: [PATCH 8/8] Mention inheriting from a generic in ways to define a class as generic. --- docs/spec/generics.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index 7ad83384..c7a61594 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -93,11 +93,14 @@ This is equivalent to omitting the generic notation and just saying User-defined generic types -------------------------- -You can define a user-defined class as generic in three ways: by including a -``Generic`` base class, by using the new generic class syntax in Python 3.12 -and higher, or by including a ``Protocol`` base class parameterized with type -variables. The third approach also marks the class as a protocol - see -:ref:`generic protocols` for more information. +There are several ways to define a user-defined class as generic: + +* Include a ``Generic`` base class. +* Use the new generic class syntax in Python 3.12 and higher. +* Include a `` Protocol`` base class parameterized with type variables. This + approach also marks the class as a protocol - see + :ref:`generic protocols` for more information. +* Include a generic base class parameterized with type variables. Example using ``Generic``::