Skip to content

Commit cb2ec3d

Browse files
committed
Improve protocols documentation (#14577)
Linking #13681
1 parent 2cfc46c commit cb2ec3d

File tree

1 file changed

+76
-13
lines changed

1 file changed

+76
-13
lines changed

docs/source/protocols.rst

+76-13
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ Protocols and structural subtyping
44
==================================
55

66
Mypy supports two ways of deciding whether two classes are compatible
7-
as types: nominal subtyping and structural subtyping. *Nominal*
8-
subtyping is strictly based on the class hierarchy. If class ``D``
7+
as types: nominal subtyping and structural subtyping.
8+
9+
*Nominal* subtyping is strictly based on the class hierarchy. If class ``D``
910
inherits class ``C``, it's also a subtype of ``C``, and instances of
1011
``D`` can be used when ``C`` instances are expected. This form of
1112
subtyping is used by default in mypy, since it's easy to understand
1213
and produces clear and concise error messages, and since it matches
1314
how the native :py:func:`isinstance <isinstance>` check works -- based on class
14-
hierarchy. *Structural* subtyping can also be useful. Class ``D`` is
15+
hierarchy.
16+
17+
*Structural* subtyping is based on the operations that can be performed with an object. Class ``D`` is
1518
a structural subtype of class ``C`` if the former has all attributes
1619
and methods of the latter, and with compatible types.
1720

@@ -72,15 +75,16 @@ class:
7275
from typing_extensions import Protocol
7376
7477
class SupportsClose(Protocol):
75-
def close(self) -> None:
76-
... # Empty method body (explicit '...')
78+
# Empty method body (explicit '...')
79+
def close(self) -> None: ...
7780
7881
class Resource: # No SupportsClose base class!
79-
# ... some methods ...
8082
8183
def close(self) -> None:
8284
self.resource.release()
8385
86+
# ... other methods ...
87+
8488
def close_all(items: Iterable[SupportsClose]) -> None:
8589
for item in items:
8690
item.close()
@@ -146,7 +150,9 @@ present if you are defining a protocol:
146150
147151
You can also include default implementations of methods in
148152
protocols. If you explicitly subclass these protocols you can inherit
149-
these default implementations. Explicitly including a protocol as a
153+
these default implementations.
154+
155+
Explicitly including a protocol as a
150156
base class is also a way of documenting that your class implements a
151157
particular protocol, and it forces mypy to verify that your class
152158
implementation is actually compatible with the protocol. In particular,
@@ -157,12 +163,62 @@ abstract:
157163
158164
class SomeProto(Protocol):
159165
attr: int # Note, no right hand side
160-
def method(self) -> str: ... # Literal ... here
166+
def method(self) -> str: ... # Literally just ... here
167+
161168
class ExplicitSubclass(SomeProto):
162169
pass
170+
163171
ExplicitSubclass() # error: Cannot instantiate abstract class 'ExplicitSubclass'
164172
# with abstract attributes 'attr' and 'method'
165173
174+
Invariance of protocol attributes
175+
*********************************
176+
177+
A common issue with protocols is that protocol attributes are invariant.
178+
For example:
179+
180+
.. code-block:: python
181+
182+
class Box(Protocol):
183+
content: object
184+
185+
class IntBox:
186+
content: int
187+
188+
def takes_box(box: Box) -> None: ...
189+
190+
takes_box(IntBox()) # error: Argument 1 to "takes_box" has incompatible type "IntBox"; expected "Box"
191+
# note: Following member(s) of "IntBox" have conflicts:
192+
# note: content: expected "object", got "int"
193+
194+
This is because ``Box`` defines ``content`` as a mutable attribute.
195+
Here's why this is problematic:
196+
197+
.. code-block:: python
198+
199+
def takes_box_evil(box: Box) -> None:
200+
box.content = "asdf" # This is bad, since box.content is supposed to be an object
201+
202+
my_int_box = IntBox()
203+
takes_box_evil(my_int_box)
204+
my_int_box.content + 1 # Oops, TypeError!
205+
206+
This can be fixed by declaring ``content`` to be read-only in the ``Box``
207+
protocol using ``@property``:
208+
209+
.. code-block:: python
210+
211+
class Box(Protocol):
212+
@property
213+
def content(self) -> object: ...
214+
215+
class IntBox:
216+
content: int
217+
218+
def takes_box(box: Box) -> None: ...
219+
220+
takes_box(IntBox(42)) # OK
221+
166222
Recursive protocols
167223
*******************
168224

@@ -197,7 +253,7 @@ Using isinstance() with protocols
197253

198254
You can use a protocol class with :py:func:`isinstance` if you decorate it
199255
with the ``@runtime_checkable`` class decorator. The decorator adds
200-
support for basic runtime structural checks:
256+
rudimentary support for runtime structural checks:
201257

202258
.. code-block:: python
203259
@@ -214,16 +270,23 @@ support for basic runtime structural checks:
214270
def use(handles: int) -> None: ...
215271
216272
mug = Mug()
217-
if isinstance(mug, Portable):
218-
use(mug.handles) # Works statically and at runtime
273+
if isinstance(mug, Portable): # Works at runtime!
274+
use(mug.handles)
219275
220276
:py:func:`isinstance` also works with the :ref:`predefined protocols <predefined_protocols>`
221277
in :py:mod:`typing` such as :py:class:`~typing.Iterable`.
222278

223-
.. note::
279+
.. warning::
224280
:py:func:`isinstance` with protocols is not completely safe at runtime.
225281
For example, signatures of methods are not checked. The runtime
226-
implementation only checks that all protocol members are defined.
282+
implementation only checks that all protocol members exist,
283+
not that they have the correct type. :py:func:`issubclass` with protocols
284+
will only check for the existence of methods.
285+
286+
.. note::
287+
:py:func:`isinstance` with protocols can also be surprisingly slow.
288+
In many cases, you're better served by using :py:func:`hasattr` to
289+
check for the presence of attributes.
227290

228291
.. _callback_protocols:
229292

0 commit comments

Comments
 (0)