@@ -4,14 +4,17 @@ Protocols and structural subtyping
4
4
==================================
5
5
6
6
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 ``
9
10
inherits class ``C ``, it's also a subtype of ``C ``, and instances of
10
11
``D `` can be used when ``C `` instances are expected. This form of
11
12
subtyping is used by default in mypy, since it's easy to understand
12
13
and produces clear and concise error messages, and since it matches
13
14
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
15
18
a structural subtype of class ``C `` if the former has all attributes
16
19
and methods of the latter, and with compatible types.
17
20
@@ -72,15 +75,16 @@ class:
72
75
from typing_extensions import Protocol
73
76
74
77
class SupportsClose (Protocol ):
75
- def close ( self ) -> None :
76
- ... # Empty method body (explicit ' ...')
78
+ # Empty method body (explicit '...')
79
+ def close ( self ) -> None : ...
77
80
78
81
class Resource : # No SupportsClose base class!
79
- # ... some methods ...
80
82
81
83
def close (self ) -> None :
82
84
self .resource.release()
83
85
86
+ # ... other methods ...
87
+
84
88
def close_all (items : Iterable[SupportsClose]) -> None :
85
89
for item in items:
86
90
item.close()
@@ -146,7 +150,9 @@ present if you are defining a protocol:
146
150
147
151
You can also include default implementations of methods in
148
152
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
150
156
base class is also a way of documenting that your class implements a
151
157
particular protocol, and it forces mypy to verify that your class
152
158
implementation is actually compatible with the protocol. In particular,
@@ -157,12 +163,62 @@ abstract:
157
163
158
164
class SomeProto (Protocol ):
159
165
attr: int # Note, no right hand side
160
- def method (self ) -> str : ... # Literal ... here
166
+ def method (self ) -> str : ... # Literally just ... here
167
+
161
168
class ExplicitSubclass (SomeProto ):
162
169
pass
170
+
163
171
ExplicitSubclass() # error: Cannot instantiate abstract class 'ExplicitSubclass'
164
172
# with abstract attributes 'attr' and 'method'
165
173
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
+
166
222
Recursive protocols
167
223
*******************
168
224
@@ -197,7 +253,7 @@ Using isinstance() with protocols
197
253
198
254
You can use a protocol class with :py:func: `isinstance ` if you decorate it
199
255
with the ``@runtime_checkable `` class decorator. The decorator adds
200
- support for basic runtime structural checks:
256
+ rudimentary support for runtime structural checks:
201
257
202
258
.. code-block :: python
203
259
@@ -214,16 +270,23 @@ support for basic runtime structural checks:
214
270
def use (handles : int ) -> None : ...
215
271
216
272
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)
219
275
220
276
:py:func: `isinstance ` also works with the :ref: `predefined protocols <predefined_protocols >`
221
277
in :py:mod: `typing ` such as :py:class: `~typing.Iterable `.
222
278
223
- .. note ::
279
+ .. warning ::
224
280
:py:func: `isinstance ` with protocols is not completely safe at runtime.
225
281
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.
227
290
228
291
.. _callback_protocols :
229
292
0 commit comments