Update PEP 544 (#644)

* Rename `@runtime` to `@runtime_checkable`
* Postpone making mappings and sequences protocols, this can be done later in Python 3.8
* Update the implementation status
* More strict and precise specification for `isinstance()` and `issubclass()` with protocols.
* Few typos
This commit is contained in:
Ivan Levkivskyi 2018-05-10 17:55:54 -04:00 committed by Guido van Rossum
parent 86c2708d7d
commit 9c06059758
1 changed files with 62 additions and 65 deletions

View File

@ -199,8 +199,8 @@ approaches related to structural subtyping in Python and other languages:
Such behavior seems to be a perfect fit for both runtime and static behavior Such behavior seems to be a perfect fit for both runtime and static behavior
of protocols. As discussed in `rationale`_, we propose to add static support of protocols. As discussed in `rationale`_, we propose to add static support
for such behavior. In addition, to allow users to achieve such runtime for such behavior. In addition, to allow users to achieve such runtime
behavior for *user-defined* protocols a special ``@runtime`` decorator will behavior for *user-defined* protocols a special ``@runtime_checkable`` decorator
be provided, see detailed `discussion`_ below. will be provided, see detailed `discussion`_ below.
* TypeScript [typescript]_ provides support for user-defined classes and * TypeScript [typescript]_ provides support for user-defined classes and
interfaces. Explicit implementation declaration is not required and interfaces. Explicit implementation declaration is not required and
@ -381,8 +381,7 @@ Explicitly declaring implementation
To explicitly declare that a certain class implements a given protocol, To explicitly declare that a certain class implements a given protocol,
it can be used as a regular base class. In this case a class could use it can be used as a regular base class. In this case a class could use
default implementations of protocol members. ``typing.Sequence`` is a good default implementations of protocol members. Static analysis tools are
example of a protocol with useful default methods. Static analysis tools are
expected to automatically detect that a class implements a given protocol. expected to automatically detect that a class implements a given protocol.
So while it's possible to subclass a protocol explicitly, it's *not necessary* So while it's possible to subclass a protocol explicitly, it's *not necessary*
to do so for the sake of type-checking. to do so for the sake of type-checking.
@ -665,14 +664,14 @@ classes. For example::
One can use multiple inheritance to define an intersection of protocols. One can use multiple inheritance to define an intersection of protocols.
Example:: Example::
from typing import Sequence, Hashable from typing import Iterable, Hashable
class HashableFloats(Sequence[float], Hashable, Protocol): class HashableFloats(Iterable[float], Hashable, Protocol):
pass pass
def cached_func(args: HashableFloats) -> float: def cached_func(args: HashableFloats) -> float:
... ...
cached_func((1, 2, 3)) # OK, tuple is both hashable and sequence cached_func((1, 2, 3)) # OK, tuple is both hashable and iterable
If this will prove to be a widely used scenario, then a special If this will prove to be a widely used scenario, then a special
intersection type construct could be added in future as specified by PEP 483, intersection type construct could be added in future as specified by PEP 483,
@ -740,8 +739,8 @@ aliases::
.. _discussion: .. _discussion:
``@runtime`` decorator and narrowing types by ``isinstance()`` ``@runtime_checkable`` decorator and narrowing types by ``isinstance()``
-------------------------------------------------------------- ------------------------------------------------------------------------
The default semantics is that ``isinstance()`` and ``issubclass()`` fail The default semantics is that ``isinstance()`` and ``issubclass()`` fail
for protocol types. This is in the spirit of duck typing -- protocols for protocol types. This is in the spirit of duck typing -- protocols
@ -753,37 +752,57 @@ instance and class checks when this makes sense, similar to how ``Iterable``
and other ABCs in ``collections.abc`` and ``typing`` already do it, and other ABCs in ``collections.abc`` and ``typing`` already do it,
but this is limited to non-generic and unsubscripted generic protocols but this is limited to non-generic and unsubscripted generic protocols
(``Iterable`` is statically equivalent to ``Iterable[Any]``). (``Iterable`` is statically equivalent to ``Iterable[Any]``).
The ``typing`` module will define a special ``@runtime`` class decorator The ``typing`` module will define a special ``@runtime_checkable`` class decorator
that provides the same semantics for class and instance checks as for that provides the same semantics for class and instance checks as for
``collections.abc`` classes, essentially making them "runtime protocols":: ``collections.abc`` classes, essentially making them "runtime protocols"::
from typing import runtime, Protocol from typing import runtime, Protocol
@runtime @runtime_checkable
class Closable(Protocol): class SupportsClose(Protocol):
def close(self): def close(self):
... ...
assert isinstance(open('some/file'), Closable) assert isinstance(open('some/file'), SupportsClose)
Static type checkers will understand ``isinstance(x, Proto)`` and
``issubclass(C, Proto)`` for protocols defined with this decorator (as they
already do for ``Iterable`` etc.). Static type checkers will narrow types
after such checks by the type erased ``Proto`` (i.e. with all variables
having type ``Any`` and all methods having type ``Callable[..., Any]``).
Note that ``isinstance(x, Proto[int])`` etc. will always fail in agreement
with PEP 484. Examples::
from typing import Iterable, Iterator, Sequence
def process(items: Iterable[int]) -> None:
if isinstance(items, Iterator):
# 'items' has type 'Iterator[int]' here
elif isinstance(items, Sequence[int]):
# Error! Can't use 'isinstance()' with subscripted protocols
Note that instance checks are not 100% reliable statically, this is why Note that instance checks are not 100% reliable statically, this is why
this behavior is opt-in, see section on `rejected`_ ideas for examples. this behavior is opt-in, see section on `rejected`_ ideas for examples.
The most type checkers can do is to treat ``isinstance(obj, Iterator)``
roughly as a simpler way to write
``hasattr(x, '__iter__') and hasattr(x, '__next__')``. To minimize
the risks for this feature, the following rules are applied.
**Definitions**:
* *Data, and non-data protocols*: A protocol is called non-data protocol
if it only contains methods as members (for example ``Sized``,
``Iterator``, etc). A protocol that contains at least one non-method member
(like ``x: int``) is called a data protocol.
* *Unsafe overlap*: A type ``X`` is called unsafely overlapping with
a protocol ``P``, if ``X`` is not a subtype of ``P``, but it is a subtype
of the type erased version of ``P`` where all members have type ``Any``.
In addition, if at least one element of a union unsafely overlaps with
a protocol ``P``, then the whole union is unsafely overlapping with ``P``.
**Specification**:
* A protocol can be used as a second argument in ``isinstance()`` and
``issubclass()`` only if it is explicitly opt-in by ``@runtime_checkable``
decorator. This requirement exists because protocol checks are not type safe
in case of dynamically set attributes, and because type checkers can only prove
that an ``isinstance()`` check is safe only for a given class, not for all its
subclasses.
* ``isinstance()`` can be used with both data and non-data protocols, while
``issubclass()`` can be used only with non-data protocols. This restriction
exists because some data attributes can be set on an instance in constructor
and this information is not always available on the class object.
* Type checkers should reject an ``isinstance()`` or ``issubclass()`` call, if
there is an unsafe overlap between the type of the first argument and
the protocol.
* Type checkers should be able to select a correct element from a union after
a safe ``isinstance()`` or ``issubclass()`` call. For narrowing from non-union
types, type checkers can use their best judgement (this is intentionally
unspecified, since a precise specification would require intersection types).
Using Protocols in Python 2.7 - 3.5 Using Protocols in Python 2.7 - 3.5
@ -825,14 +844,12 @@ effects on the core interpreter and standard library except in the
a protocol or not. Add a class attribute ``_is_protocol = True`` a protocol or not. Add a class attribute ``_is_protocol = True``
if that is the case. Verify that a protocol class only has protocol if that is the case. Verify that a protocol class only has protocol
base classes in the MRO (except for object). base classes in the MRO (except for object).
* Implement ``@runtime`` that allows ``__subclasshook__()`` performing * Implement ``@runtime_checkable`` that allows ``__subclasshook__()``
structural instance and subclass checks as in ``collections.abc`` classes. performing structural instance and subclass checks as in ``collections.abc``
classes.
* All structural subtyping checks will be performed by static type checkers, * All structural subtyping checks will be performed by static type checkers,
such as ``mypy`` [mypy]_. No additional support for protocol validation will such as ``mypy`` [mypy]_. No additional support for protocol validation will
be provided at runtime. be provided at runtime.
* Classes ``Mapping``, ``MutableMapping``, ``Sequence``, and
``MutableSequence`` in ``collections.abc`` module will support structural
instance and subclass checks (like e.g. ``collections.abc.Iterable``).
Changes in the typing module Changes in the typing module
@ -849,8 +866,6 @@ The following classes in ``typing`` module will be protocols:
* ``Container`` * ``Container``
* ``Collection`` * ``Collection``
* ``Reversible`` * ``Reversible``
* ``Sequence``, ``MutableSequence``
* ``Mapping``, ``MutableMapping``
* ``ContextManager``, ``AsyncContextManager`` * ``ContextManager``, ``AsyncContextManager``
* ``SupportsAbs`` (and other ``Supports*`` classes) * ``SupportsAbs`` (and other ``Supports*`` classes)
@ -1026,11 +1041,10 @@ be considered "non-protocol". Therefore, it was decided to not introduce
"non-protocol" methods. "non-protocol" methods.
There is only one downside to this: it will require some boilerplate for There is only one downside to this: it will require some boilerplate for
implicit subtypes of ``Mapping`` and few other "large" protocols. But, this implicit subtypes of "large" protocols. But, this doesn't apply to "built-in"
applies to few "built-in" protocols (like ``Mapping`` and ``Sequence``) and protocols that are all "small" (i.e. have only few abstract methods).
people are already subclassing them. Also, such style is discouraged for Also, such style is discouraged for user-defined protocols. It is recommended
user-defined protocols. It is recommended to create compact protocols and to create compact protocols and combine them.
combine them.
Make protocols interoperable with other approaches Make protocols interoperable with other approaches
@ -1103,7 +1117,7 @@ Another potentially problematic case is assignment of attributes
self.x = 0 self.x = 0
c = C() c = C()
isinstance(c1, P) # False isinstance(c, P) # False
c.initialize() c.initialize()
isinstance(c, P) # True isinstance(c, P) # True
@ -1149,7 +1163,7 @@ This was rejected for the following reasons:
ABCs from ``typing`` module. If we prohibit explicit subclassing of these ABCs from ``typing`` module. If we prohibit explicit subclassing of these
ABCs, then quite a lot of code will break. ABCs, then quite a lot of code will break.
* Convenience: There are existing protocol-like ABCs (that will be turned * Convenience: There are existing protocol-like ABCs (that may be turned
into protocols) that have many useful "mix-in" (non-abstract) methods. into protocols) that have many useful "mix-in" (non-abstract) methods.
For example in the case of ``Sequence`` one only needs to implement For example in the case of ``Sequence`` one only needs to implement
``__getitem__`` and ``__len__`` in an explicit subclass, and one gets ``__getitem__`` and ``__len__`` in an explicit subclass, and one gets
@ -1301,33 +1315,16 @@ confusions.
Backwards Compatibility Backwards Compatibility
======================= =======================
This PEP is almost fully backwards compatible. Few collection classes such as This PEP is fully backwards compatible.
``Sequence`` and ``Mapping`` will be turned into runtime protocols, therefore
results of ``isinstance()`` checks are going to change in some edge cases.
For example, a class that implements the ``Sequence`` protocol but does not
explicitly inherit from ``Sequence`` currently returns ``False`` in
corresponding instance and class checks. With this PEP implemented, such
checks will return ``True``.
Implementation Implementation
============== ==============
A working implementation of this PEP for ``mypy`` type checker is found on The ``mypy`` type checker fully supports protocols (modulo a few
GitHub repo at https://github.com/ilevkivskyi/mypy/tree/protocols, known bugs). This includes treating all the builtin protocols, such as
corresponding ``typeshed`` stubs for more flavor are found at ``Iterable`` structurally. The runtime implementation of protocols is
https://github.com/ilevkivskyi/typeshed/tree/protocols. Installation steps:: available in ``typing_extensions`` module on PyPI.
git clone --recurse-submodules https://github.com/ilevkivskyi/mypy/
cd mypy && git checkout protocols && cd typeshed
git remote add proto https://github.com/ilevkivskyi/typeshed
git fetch proto && git checkout proto/protocols
cd .. && git add typeshed && sudo python3 -m pip install -U .
The runtime implementation of protocols in ``typing`` module is
found at https://github.com/ilevkivskyi/typehinting/tree/protocols.
The version of ``collections.abc`` with structural behavior for mappings and
sequences is found at https://github.com/ilevkivskyi/cpython/tree/protocols.
References References