@@ -4,14 +4,17 @@ Protocols and structural subtyping
44==================================
55
66Mypy 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 ``
910inherits 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
1112subtyping is used by default in mypy, since it's easy to understand
1213and produces clear and concise error messages, and since it matches
1314how 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
1518a structural subtype of class ``C `` if the former has all attributes
1619and 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
148152protocols. 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
150156base class is also a way of documenting that your class implements a
151157particular protocol, and it forces mypy to verify that your class
152158implementation 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
198254You can use a protocol class with :py:func: `isinstance ` if you decorate it
199255with 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 >`
221277in :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