From 12abe2e599fc6fe2c0f4b9f6bc3432203e113e35 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 1 Feb 2023 11:31:04 -0800 Subject: [PATCH 1/3] Improve protocols documentation Linking #13681 --- docs/source/protocols.rst | 88 +++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index 603c9fd0dcc80..4de7724f3c95e 100644 --- a/docs/source/protocols.rst +++ b/docs/source/protocols.rst @@ -4,14 +4,17 @@ Protocols and structural subtyping ================================== Mypy supports two ways of deciding whether two classes are compatible -as types: nominal subtyping and structural subtyping. *Nominal* -subtyping is strictly based on the class hierarchy. If class ``D`` +as types: nominal subtyping and structural subtyping. + +*Nominal* subtyping is strictly based on the class hierarchy. If class ``D`` inherits class ``C``, it's also a subtype of ``C``, and instances of ``D`` can be used when ``C`` instances are expected. This form of subtyping is used by default in mypy, since it's easy to understand and produces clear and concise error messages, and since it matches how the native :py:func:`isinstance ` check works -- based on class -hierarchy. *Structural* subtyping can also be useful. Class ``D`` is +hierarchy. + +*Structural* subtyping can also be useful. Class ``D`` is a structural subtype of class ``C`` if the former has all attributes and methods of the latter, and with compatible types. @@ -72,15 +75,16 @@ class: from typing_extensions import Protocol class SupportsClose(Protocol): - def close(self) -> None: - ... # Empty method body (explicit '...') + # Empty method body (explicit '...') + def close(self) -> None: ... class Resource: # No SupportsClose base class! - # ... some methods ... def close(self) -> None: self.resource.release() + # ... other methods ... + def close_all(items: Iterable[SupportsClose]) -> None: for item in items: item.close() @@ -146,7 +150,9 @@ present if you are defining a protocol: You can also include default implementations of methods in protocols. If you explicitly subclass these protocols you can inherit -these default implementations. Explicitly including a protocol as a +these default implementations. + +Explicitly including a protocol as a base class is also a way of documenting that your class implements a particular protocol, and it forces mypy to verify that your class implementation is actually compatible with the protocol. In particular, @@ -157,12 +163,62 @@ abstract: class SomeProto(Protocol): attr: int # Note, no right hand side - def method(self) -> str: ... # Literal ... here + def method(self) -> str: ... # Literally just ... here + class ExplicitSubclass(SomeProto): pass + ExplicitSubclass() # error: Cannot instantiate abstract class 'ExplicitSubclass' # with abstract attributes 'attr' and 'method' +Invariance of protocol attributes +********************************* + +A common issue with protocols is that protocol attributes are invariant. +For example: + +.. code-block:: python + + class Box(Protocol): + content: object + + class IntBox: + content: int + + def takes_box(box: Box) -> None: ... + + takes_box(IntBox()) # error: Argument 1 to "takes_box" has incompatible type "IntBox"; expected "Box" + # note: Following member(s) of "IntBox" have conflicts: + # note: content: expected "object", got "int" + +This is because ``Box`` defines ``content`` as a mutable attribute. +Here's why this is problematic: + +.. code-block:: python + + def takes_box_evil(box: Box) -> None: + box.content = "asdf" # This is bad, since box.content is supposed to be an object + + my_int_box = IntBox() + takes_box_evil(my_int_box) + my_int_box.content + 1 # Oops, TypeError! + +This can be fixed by declaring ``content`` to be read-only in the ``Box`` +protocol using ``@property``: + +.. code-block:: python + + class Box(Protocol): + @property + def content(self) -> object: ... + + class IntBox: + content: int + + def takes_box(box: Box) -> None: ... + + takes_box(IntBox(42)) # OK + Recursive protocols ******************* @@ -197,7 +253,7 @@ Using isinstance() with protocols You can use a protocol class with :py:func:`isinstance` if you decorate it with the ``@runtime_checkable`` class decorator. The decorator adds -support for basic runtime structural checks: +rudimentary support for runtime structural checks: .. code-block:: python @@ -214,16 +270,22 @@ support for basic runtime structural checks: def use(handles: int) -> None: ... mug = Mug() - if isinstance(mug, Portable): - use(mug.handles) # Works statically and at runtime + if isinstance(mug, Portable): # Works at runtime! + use(mug.handles) :py:func:`isinstance` also works with the :ref:`predefined protocols ` in :py:mod:`typing` such as :py:class:`~typing.Iterable`. -.. note:: +.. warning:: :py:func:`isinstance` with protocols is not completely safe at runtime. For example, signatures of methods are not checked. The runtime - implementation only checks that all protocol members are defined. + implementation only checks that all protocol members exist, + not that they have the correct type. + +.. note:: + :py:func:`isinstance` with protocols can also be surprisingly slow. + In many cases, you're better served by using :py:func:`hasattr` to + check for the presence of attributes. .. _callback_protocols: From c27150bba19272ed4abd736975c54562ffbc58a5 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 2 Feb 2023 12:09:10 -0800 Subject: [PATCH 2/3] Update docs/source/protocols.rst Co-authored-by: Jelle Zijlstra --- docs/source/protocols.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index 4de7724f3c95e..950babd4c4909 100644 --- a/docs/source/protocols.rst +++ b/docs/source/protocols.rst @@ -14,7 +14,7 @@ and produces clear and concise error messages, and since it matches how the native :py:func:`isinstance ` check works -- based on class hierarchy. -*Structural* subtyping can also be useful. Class ``D`` is +*Structural* subtyping is based on the operations that can be performed with an object. Class ``D`` is a structural subtype of class ``C`` if the former has all attributes and methods of the latter, and with compatible types. From 4d4f02f037f2e70bea6623d0fdbc555c08ed8242 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 2 Feb 2023 12:37:07 -0800 Subject: [PATCH 3/3] Mention issubclass --- docs/source/protocols.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index 950babd4c4909..cb51809a66d50 100644 --- a/docs/source/protocols.rst +++ b/docs/source/protocols.rst @@ -280,7 +280,8 @@ in :py:mod:`typing` such as :py:class:`~typing.Iterable`. :py:func:`isinstance` with protocols is not completely safe at runtime. For example, signatures of methods are not checked. The runtime implementation only checks that all protocol members exist, - not that they have the correct type. + not that they have the correct type. :py:func:`issubclass` with protocols + will only check for the existence of methods. .. note:: :py:func:`isinstance` with protocols can also be surprisingly slow.