Skip to content

Commit 4205604

Browse files
ilevkivskyigvanrossum
authored andcommitted
Updates to PEP 544: Protocols (#255)
* Add covariant mutable overriding and overriding variance to rejected ideas * Update the notes on runtime implementation * Add one more argument for prohibiting variance overrides
1 parent 384ff42 commit 4205604

File tree

1 file changed

+103
-14
lines changed

1 file changed

+103
-14
lines changed

pep-0544.txt

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -507,24 +507,29 @@ non-protocol generic types::
507507
``Protocol[T, S, ...]`` is allowed as a shorthand for
508508
``Protocol, Generic[T, S, ...]``.
509509

510-
Declaring variance is not necessary for protocol classes, since it can be
511-
inferred from a protocol definition. Examples::
510+
User-defined generic protocols support explicitly declared variance.
511+
Type checkers will warn if the inferred variance is different from
512+
the declared variance. Examples::
512513

513-
class Box(Protocol[T]):
514-
def content(self) -> T:
514+
T = TypeVar('T')
515+
T_co = TypeVar('T_co', covariant=True)
516+
T_contra = TypeVar('T_contra', contravariant=True)
517+
518+
class Box(Protocol[T_co]):
519+
def content(self) -> T_co:
515520
...
516521

517522
box: Box[float]
518523
second_box: Box[int]
519-
box = second_box # This is OK due to the inferred covariance of 'Box'.
524+
box = second_box # This is OK due to the covariance of 'Box'.
520525

521-
class Sender(Protocol[T]):
522-
def send(self, data: T) -> int:
526+
class Sender(Protocol[T_contra]):
527+
def send(self, data: T_contra) -> int:
523528
...
524529

525530
sender: Sender[float]
526531
new_sender: Sender[int]
527-
new_sender = sender # OK, type checker finds that 'Sender' is contravariant.
532+
new_sender = sender # OK, 'Sender' is contravariant.
528533

529534
class Proto(Protocol[T]):
530535
attr: T # this class is invariant, since it has a mutable attribute
@@ -533,6 +538,16 @@ inferred from a protocol definition. Examples::
533538
another_var: Proto[int]
534539
var = another_var # Error! 'Proto[float]' is incompatible with 'Proto[int]'.
535540

541+
Note that unlike nominal classes, de-facto covariant protocols cannot be
542+
declared as invariant, since this can break transitivity of subtyping
543+
(see `rejected`_ ideas for details). For example::
544+
545+
T = TypeVar('T')
546+
547+
class AnotherBox(Protocol[T]): # Error, this protocol is covariant in T,
548+
def content(self) -> T: # not invariant.
549+
...
550+
536551

537552
Recursive protocols
538553
-------------------
@@ -562,7 +577,7 @@ Continuing the previous example::
562577

563578
def walk(graph: Traversable) -> None:
564579
...
565-
tree: Tree[float] = Tree(0, [])
580+
tree: Tree[float] = Tree()
566581
walk(tree) # OK, 'Tree[float]' is a subtype of 'Traversable'
567582

568583

@@ -771,17 +786,21 @@ Implementation details
771786

772787
The runtime implementation could be done in pure Python without any
773788
effects on the core interpreter and standard library except in the
774-
``typing`` module:
789+
``typing`` module, and a minor update to ``collections.abc``:
775790

776791
* Define class ``typing.Protocol`` similar to ``typing.Generic``.
777792
* Implement metaclass functionality to detect whether a class is
778-
a protocol or not. Add a class attribute ``__protocol__ = True``
793+
a protocol or not. Add a class attribute ``_is_protocol = True``
779794
if that is the case. Verify that a protocol class only has protocol
780795
base classes in the MRO (except for object).
781-
* Implement ``@runtime`` that adds all attributes to ``__subclasshook__()``.
796+
* Implement ``@runtime`` that allows ``__subclasshook__()`` performing
797+
structural instance and subclass checks as in ``collections.abc`` classes.
782798
* All structural subtyping checks will be performed by static type checkers,
783799
such as ``mypy`` [mypy]_. No additional support for protocol validation will
784800
be provided at runtime.
801+
* Classes ``Mapping``, ``MutableMapping``, ``Sequence``, and
802+
``MutableSequence`` in ``collections.abc`` module will support structural
803+
instance and subclass checks (like e.g. ``collections.abc.Iterable``).
785804

786805

787806
Changes in the typing module
@@ -879,8 +898,8 @@ reasons:
879898
Python runtime, which won't happen.
880899

881900

882-
Allow protocols subclassing normal classes
883-
------------------------------------------
901+
Protocols subclassing normal classes
902+
------------------------------------
884903

885904
The main rationale to prohibit this is to preserve transitivity of subtyping,
886905
consider this example::
@@ -1118,6 +1137,74 @@ This was rejected for the following reasons:
11181137
it has an unsafe override.
11191138

11201139

1140+
Covariant subtyping of mutable attributes
1141+
-----------------------------------------
1142+
1143+
Rejected because covariant subtyping of mutable attributes is not safe.
1144+
Consider this example::
1145+
1146+
class P(Protocol):
1147+
x: float
1148+
1149+
def f(arg: P) -> None:
1150+
arg.x = 0.42
1151+
1152+
class C:
1153+
x: int
1154+
1155+
c = C()
1156+
f(c) # Would typecheck if covariant subtyping
1157+
# of mutable attributes were allowed
1158+
c.x >> 1 # But this fails at runtime
1159+
1160+
It was initially proposed to allow this for practical reasons, but it was
1161+
subsequently rejected, since this may mask some hard to spot bugs.
1162+
1163+
1164+
Overriding inferred variance of protocol classes
1165+
------------------------------------------------
1166+
1167+
It was proposed to allow declaring protocols as invariant if they are actually
1168+
covariant or contravariant (as it is possible for nominal classes, see PEP 484).
1169+
However, it was decided not to do this because of several downsides:
1170+
1171+
* Declared protocol invariance breaks transitivity of sub-typing. Consider
1172+
this situation::
1173+
1174+
T = TypeVar('T')
1175+
1176+
class P(Protocol[T]): # Declared as invariant
1177+
def meth(self) -> T:
1178+
...
1179+
class C:
1180+
def meth(self) -> float:
1181+
...
1182+
class D(C):
1183+
def meth(self) -> int:
1184+
...
1185+
1186+
Now we have that ``D`` is a subtype of ``C``, and ``C`` is a subtype of
1187+
``P[float]``. But ``D`` is *not* a subtype of ``P[float]`` since ``D``
1188+
implements ``P[int]``, and ``P`` is invariant. There is a possibility
1189+
to "cure" this by looking for protocol implementations in MROs but this
1190+
will be too complex in a general case, and this "cure" requires abandoning
1191+
simple idea of purely structural subtyping for protocols.
1192+
1193+
* Subtyping checks will always require type inference for protocols. In the
1194+
above example a user may complain: "Why did you infer ``P[int]`` for
1195+
my ``D``? It implements ``P[float]``!". Normally, inference can be overruled
1196+
by an explicit annotation, but here this will require explicit subclassing,
1197+
defeating the purpose of using protocols.
1198+
1199+
* Allowing overriding variance will make impossible more detailed error
1200+
messages in type checkers citing particular conflicts in member
1201+
type signatures.
1202+
1203+
* Finally, explicit is better than implicit in this case. Requiring user to
1204+
declare correct variance will simplify understanding the code and will avoid
1205+
unexpected errors at the point of use.
1206+
1207+
11211208
Support adapters and adaptation
11221209
-------------------------------
11231210

@@ -1179,6 +1266,8 @@ https://github.com/ilevkivskyi/typeshed/tree/protocols. Installation steps::
11791266

11801267
The runtime implementation of protocols in ``typing`` module is
11811268
found at https://github.com/ilevkivskyi/typehinting/tree/protocols.
1269+
The version of ``collections.abc`` with structural behavior for mappings and
1270+
sequences is found at https://github.com/ilevkivskyi/cpython/tree/protocols.
11821271

11831272

11841273
References

0 commit comments

Comments
 (0)