8000 Clarify some behavior around user-defined generic classes (#1879) · python/typing@438dc29 · GitHub
[go: up one dir, main page]

Skip to content

Commit 438dc29

Browse files
authored
Clarify some behavior around user-defined generic classes (#1879)
* Update generics.rst * Update protocol.rst * Address reviewer comments. * Fix silly typo * English * Incorporate additional rules suggested by @erictraut. * Mandate error on inconsistent type variable order. * Mention inheriting from a generic in ways to define a class as generic.
1 parent 46b05a4 commit 438dc29

File tree

2 files changed

+95
-19
lines changed

2 files changed

+95
-19
lines changed

docs/spec/generics.rst

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,16 @@ This is equivalent to omitting the generic notation and just saying
9393
User-defined generic types
9494
--------------------------
9595

96-
You can include a ``Generic`` base class to define a user-defined class
97-
as generic. Example::
96+
There are several ways to define a user-defined class as generic:
97+
98+
* Include a ``Generic`` base class.
99+
* Use the new generic class syntax in Python 3.12 and higher.
100+
* Include a `` Protocol`` base class parameterized with type variables. This
101+
approach also marks the class as a protocol - see
102+
:ref:`generic protocols<generic-protocols>` for more information.
103+
* Include a generic base class parameterized with type variables.
104+
105+
Example using ``Generic``::
98106

99107
from typing import TypeVar, Generic
100108
from logging import Logger
@@ -118,14 +126,14 @@ as generic. Example::
118126
def log(self, message: str) -> None:
119127
self.logger.info('{}: {}'.format(self.name, message))
120128

121-
Or, in Python 3.12 and higher, by using the new syntax for generic
122-
classes::
129+
Or, using the new generic class syntax::
123130

124131
class LoggedVar[T]:
125132
# methods as in previous example
126133

127-
This implicitly adds ``Generic[T]`` as a base class and type checkers
128-
should treat the two largely equivalently (except for variance, see below).
134+
This implicitly adds ``Generic[T]`` as a base class, and type checkers
135+
should treat the two definitions of ``LoggedVar`` largely equivalently (except
136+
for variance, see below).
129137

130138
``Generic[T]`` as a base class defines that the class ``LoggedVar``
131139
takes a single type parameter ``T``. This also makes ``T`` valid as
@@ -144,7 +152,6 @@ A generic type can have any number of type variables, and type variables
144152
may be constrained. This is valid::
145153

146154
from typing import TypeVar, Generic
147-
...
148155

149156
T = TypeVar('T')
150157
S = TypeVar('S')
@@ -156,29 +163,52 @@ Each type variable argument to ``Generic`` must be distinct. This is
156163
thus invalid::
157164

158165
from typing import TypeVar, Generic
159-
...
160166

161167
T = TypeVar('T')
162168

163169
class Pair(Generic[T, T]): # INVALID
164170
...
165171

166-
The ``Generic[T]`` base class is redundant in simple cases where you
167-
subclass some other generic class and specify type variables for its
168-
parameters::
172+
All arguments to ``Generic`` or ``Protocol`` must be type variables::
169173

170-
from typing import TypeVar
171-
from collections.abc import Iterator
174+
from typing import Generic, Protocol
175+
176+
class Bad1(Generic[int]): # INVALID
177+
...
178+
class Bad2(Protocol[int]): # INVALID
179+
...
180+
181+
When a ``Generic`` or parameterized ``Protocol`` base class is present, all type
182+
parameters for the class must appear within the ``Generic`` or
183+
``Protocol`` type argument list, respectively. A type checker should report an
184+
error if a type variable that is not included in the type argument list appears
185+
elsewhere in the base class list::
186+
187+
from typing import Generic, Protocol, TypeVar
188+
from collections.abc import Iterable
172189

173190
T = TypeVar('T')
191+
S = TypeVar('S')
174192

175-
class MyIter(Iterator[T]):
193+
class Bad1(Iterable[T], Generic[S]): # INVALID
194+
...
195+
class Bad2(Iterable[T], Protocol[S]): # INVALID
176196
...
177197

178-
That class definition is equivalent to::
198+
Note that the above rule does not apply to a bare ``Protocol`` base class. This
199+
is valid (see below)::
179200

180-
class MyIter(Iterator[T], Generic[T]):
181-
...
201+
from typing import Protocol, TypeVar
202+
from collections.abc import Iterator
203+
204+
T = TypeVar('T')
205+
206+
class MyIterator(Iterator[T], Protocol): ...
207+
208+
When no ``Generic`` or parameterized ``Protocol`` base class is present, a
209+
defined class is generic if you subclass one or more other generic classes and
210+
specify type variables for their parameters. See :ref:`generic-base-classes`
211+
for details.
182212

183213
You can use multiple inheritance with ``Generic``::
184214

@@ -402,6 +432,7 @@ instead is preferred. (First, creating the subscripted class,
402432
e.g. ``Node[int]``, has a runtime cost. Second, using a type alias
403433
is more readable.)
404434

435+
.. _`generic-base-classes`:
405436

406437
Arbitrary generic types as base classes
407438
---------------------------------------
@@ -458,8 +489,44 @@ Also consider the following example::
458489
class MyDict(Mapping[str, T]):
459490
...
460491

461-
In this case MyDict has a single parameter, T.
492+
In this case ``MyDict`` has a single type parameter, ``T``.
493+
494+
Type variables are applied to the defined class in the order in which
495+
they first appear in any generic base classes::
496+
497+
from typing import Generic, TypeVar
498+
499+
T1 = TypeVar('T1')
500+
T2 = TypeVar('T2')
501+
T3 = TypeVar('T3')
502+
503+
class Parent1(Generic[T1, T2]):
504+
...
505+
class Parent2(Generic[T1, T2]):
506+
...
507+
class Child(Parent1[T1, T3], Parent2[T2, T3]):
508+
...
509+
510+
That ``Child`` definition is equivalent to::
462511

512+
class Child(Parent1[T1, T3], Parent2[T2, T3], Generic[T1, T3, T2]):
513+
...
514+
515+
A type checker should report an error when the type variable order is
516+
inconsistent::
517+
518+
from typing import Generic, TypeVar
519+
520+
T1 = TypeVar('T1')
521+
T2 = TypeVar('T2')
522+
T3 = TypeVar('T3')
523+
524+
class Grandparent(Generic[T1, T2]):
525+
...
526+
class Parent(Grandparent[T1, T2]):
527+
...
528+
class Child(Parent[T1, T2], Grandparent[T2, T1]): # INVALID
529+
...
463530

464531
Abstract generic types
465532
----------------------

docs/spec/protocol.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ from regular ABCs, where abstractness is simply defined by having at least one
257257
abstract method being unimplemented. Protocol classes must be marked
258258
*explicitly*.
259259

260+
.. _`generic-protocols`:
260261

261262
Generic protocols
262263
^^^^^^^^^^^^^^^^^
@@ -271,7 +272,15 @@ non-protocol generic types::
271272
...
272273

273274
``Protocol[T, S, ...]`` is allowed as a shorthand for
274-
``Protocol, Generic[T, S, ...]``.
275+
``Protocol, Generic[T, S, ...]``. It is an error to combine
276+
``Protocol[T, S, ...]`` with ``Generic[T, S, ...]``, or with the new syntax for
277+
generic classes in Python 3.12 and above::
278+
279+
class Iterable(Protocol[T], Generic[T]): # INVALID
280+
...
281+
282+
class Iterable[T](Protocol[T]): # INVALID
283+
...
275284

276285
User-defined generic protocols support explicitly declared variance.
277286
Type checkers will warn if the inferred variance is different from

0 commit comments

Comments
 (0)
0