@@ -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
537552Recursive 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
772787The runtime implementation could be done in pure Python without any
773788effects 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
787806Changes 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
885904The main rationale to prohibit this is to preserve transitivity of subtyping,
886905consider 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+
11211208Support adapters and adaptation
11221209-------------------------------
11231210
@@ -1179,6 +1266,8 @@ https://github.com/ilevkivskyi/typeshed/tree/protocols. Installation steps::
11791266
11801267The runtime implementation of protocols in ``typing`` module is
11811268found 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
11841273References
0 commit comments