Skip to content

Commit c55e48b

Browse files
ilevkivskyiJukkaL
authored andcommitted
Protocols (#3132)
This is an implementation of protocols proposed in PEP 544. This PR adds support for: * generic protocols (including inference for protocols) * recursive protocols * special support for Type[] and isinstance() * structural subtyping for Callable and Tuple * other things
1 parent c295f0c commit c55e48b

35 files changed

+3684
-177
lines changed

docs/source/class_basics.rst

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,174 @@ concrete. As with normal overrides, a dynamically typed method can
151151
implement a statically typed abstract method defined in an abstract
152152
base class.
153153

154+
.. _protocol-types:
155+
156+
Protocols and structural subtyping
157+
**********************************
158+
159+
.. note::
160+
161+
The support for structural subtyping is still experimental. Some features
162+
might be not yet implemented, mypy could pass unsafe code or reject
163+
working code.
164+
165+
There are two main type systems with respect to subtyping: nominal subtyping
166+
and structural subtyping. The *nominal* subtyping is based on class hierarchy,
167+
so that if class ``D`` inherits from class ``C``, then it is a subtype
168+
of ``C``. This type system is primarily used in mypy since it allows
169+
to produce clear and concise error messages, and since Python provides native
170+
``isinstance()`` checks based on class hierarchy. The *structural* subtyping
171+
however has its own advantages. In this system class ``D`` is a subtype
172+
of class ``C`` if the former has all attributes of the latter with
173+
compatible types.
174+
175+
This type system is a static equivalent of duck typing, well known by Python
176+
programmers. Mypy provides an opt-in support for structural subtyping via
177+
protocol classes described in this section.
178+
See `PEP 544 <https://www.python.org/dev/peps/pep-0544/>`_ for
179+
specification of protocols and structural subtyping in Python.
180+
181+
User defined protocols
182+
**********************
183+
184+
To define a protocol class, one must inherit the special
185+
``typing_extensions.Protocol`` class:
186+
187+
.. code-block:: python
188+
189+
from typing import Iterable
190+
from typing_extensions import Protocol
191+
192+
class SupportsClose(Protocol):
193+
def close(self) -> None:
194+
...
195+
196+
class Resource: # Note, this class does not have 'SupportsClose' base.
197+
# some methods
198+
def close(self) -> None:
199+
self.resource.release()
200+
201+
def close_all(things: Iterable[SupportsClose]) -> None:
202+
for thing in things:
203+
thing.close()
204+
205+
close_all([Resource(), open('some/file')]) # This passes type check
206+
207+
.. note::
208+
209+
The ``Protocol`` base class is currently provided in ``typing_extensions``
210+
package. When structural subtyping is mature and
211+
`PEP 544 <https://www.python.org/dev/peps/pep-0544/>`_ is accepted,
212+
``Protocol`` will be included in the ``typing`` module. As well, several
213+
types such as ``typing.Sized``, ``typing.Iterable`` etc. will be made
214+
protocols.
215+
216+
Defining subprotocols
217+
*********************
218+
219+
Subprotocols are also supported. Existing protocols can be extended
220+
and merged using multiple inheritance. For example:
221+
222+
.. code-block:: python
223+
224+
# continuing from previous example
225+
226+
class SupportsRead(Protocol):
227+
def read(self, amount: int) -> bytes: ...
228+
229+
class TaggedReadableResource(SupportsClose, SupportsRead, Protocol):
230+
label: str
231+
232+
class AdvancedResource(Resource):
233+
def __init__(self, label: str) -> None:
234+
self.label = label
235+
def read(self, amount: int) -> bytes:
236+
# some implementation
237+
...
238+
239+
resource = None # type: TaggedReadableResource
240+
241+
# some code
242+
243+
resource = AdvancedResource('handle with care') # OK
244+
245+
Note that inheriting from existing protocols does not automatically turn
246+
a subclass into a protocol, it just creates a usual (non-protocol) ABC that
247+
implements given protocols. The ``typing_extensions.Protocol`` base must always
248+
be explicitly present:
249+
250+
.. code-block:: python
251+
252+
class NewProtocol(SupportsClose): # This is NOT a protocol
253+
new_attr: int
254+
255+
class Concrete:
256+
new_attr = None # type: int
257+
def close(self) -> None:
258+
...
259+
# Below is an error, since nominal subtyping is used by default
260+
x = Concrete() # type: NewProtocol # Error!
261+
262+
.. note::
263+
264+
The `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_ variable
265+
annotations can be used to declare protocol attributes. However, protocols
266+
are also supported on Python 2.7 and Python 3.3+ with the help of type
267+
comments and properties, see
268+
`backwards compatibility in PEP 544 <https://www.python.org/dev/peps/pep-0544/#backwards-compatibility>`_.
269+
270+
Recursive protocols
271+
*******************
272+
273+
Protocols can be recursive and mutually recursive. This could be useful for
274+
declaring abstract recursive collections such as trees and linked lists:
275+
276+
.. code-block:: python
277+
278+
from typing import TypeVar, Optional
279+
from typing_extensions import Protocol
280+
281+
class TreeLike(Protocol):
282+
value: int
283+
@property
284+
def left(self) -> Optional['TreeLike']: ...
285+
@property
286+
def right(self) -> Optional['TreeLike']: ...
287+
288+
class SimpleTree:
289+
def __init__(self, value: int) -> None:
290+
self.value = value
291+
self.left: Optional['SimpleTree'] = None
292+
self.right: Optional['SimpleTree'] = None
293+
294+
root = SimpleTree(0) # type: TreeLike # OK
295+
296+
Using ``isinstance()`` with protocols
297+
*************************************
298+
299+
To use a protocol class with ``isinstance()``, one needs to decorate it with
300+
a special ``typing_extensions.runtime`` decorator. It will add support for
301+
basic runtime structural checks:
302+
303+
.. code-block:: python
304+
305+
from typing_extensions import Protocol, runtime
306+
307+
@runtime
308+
class Portable(Protocol):
309+
handles: int
310+
311+
class Mug:
312+
def __init__(self) -> None:
313+
self.handles = 1
314+
315+
mug = Mug()
316+
if isinstance(mug, Portable):
317+
use(mug.handles) # Works statically and at runtime.
318+
154319
.. note::
320+
``isinstance()`` is with protocols not completely safe at runtime.
321+
For example, signatures of methods are not checked. The runtime
322+
implementation only checks the presence of all protocol members
323+
in object's MRO.
155324

156-
There are also plans to support more Python-style "duck typing" in
157-
the type system. The details are still open.

docs/source/common_issues.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,48 @@ Possible strategies in such situations are:
226226
return x[0]
227227
f_good(new_lst) # OK
228228
229+
Covariant subtyping of mutable protocol members is rejected
230+
-----------------------------------------------------------
231+
232+
Mypy rejects this because this is potentially unsafe.
233+
Consider this example:
234+
235+
.. code-block:: python
236+
237+
from typing_extensions import Protocol
238+
239+
class P(Protocol):
240+
x: float
241+
242+
def fun(arg: P) -> None:
243+
arg.x = 3.14
244+
245+
class C:
246+
x = 42
247+
c = C()
248+
fun(c) # This is not safe
249+
c.x << 5 # Since this will fail!
250+
251+
To work around this problem consider whether "mutating" is actually part
252+
of a protocol. If not, then one can use a ``@property`` in
253+
the protocol definition:
254+
255+
.. code-block:: python
256+
257+
from typing_extensions import Protocol
258+
259+
class P(Protocol):
260+
@property
261+
def x(self) -> float:
262+
pass
263+
264+
def fun(arg: P) -> None:
265+
...
266+
267+
class C:
268+
x = 42
269+
fun(C()) # OK
270+
229271
Declaring a supertype as variable type
230272
--------------------------------------
231273

docs/source/faq.rst

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,35 +101,38 @@ Is mypy free?
101101
Yes. Mypy is free software, and it can also be used for commercial and
102102
proprietary projects. Mypy is available under the MIT license.
103103

104-
Why not use structural subtyping?
105-
*********************************
104+
Can I use structural subtyping?
105+
*******************************
106106

107-
Mypy primarily uses `nominal subtyping
108-
<https://en.wikipedia.org/wiki/Nominative_type_system>`_ instead of
107+
Mypy provides support for both `nominal subtyping
108+
<https://en.wikipedia.org/wiki/Nominative_type_system>`_ and
109109
`structural subtyping
110-
<https://en.wikipedia.org/wiki/Structural_type_system>`_. Some argue
111-
that structural subtyping is better suited for languages with duck
112-
typing such as Python.
113-
114-
Here are some reasons why mypy uses nominal subtyping:
110+
<https://en.wikipedia.org/wiki/Structural_type_system>`_.
111+
Support for structural subtyping is considered experimental.
112+
Some argue that structural subtyping is better suited for languages with duck
113+
typing such as Python. Mypy however primarily uses nominal subtyping,
114+
leaving structural subtyping opt-in. Here are some reasons why:
115115

116116
1. It is easy to generate short and informative error messages when
117117
using a nominal type system. This is especially important when
118118
using type inference.
119119

120-
2. Python supports basically nominal isinstance tests and they are
121-
widely used in programs. It is not clear how to support isinstance
122-
in a purely structural type system while remaining compatible with
123-
Python idioms.
120+
2. Python provides built-in support for nominal ``isinstance()`` tests and
121+
they are widely used in programs. Only limited support for structural
122+
``isinstance()`` exists for ABCs in ``collections.abc`` and ``typing``
123+
standard library modules.
124124

125125
3. Many programmers are already familiar with nominal subtyping and it
126126
has been successfully used in languages such as Java, C++ and
127127
C#. Only few languages use structural subtyping.
128128

129-
However, structural subtyping can also be useful. Structural subtyping
130-
is a likely feature to be added to mypy in the future, even though we
131-
expect that most mypy programs will still primarily use nominal
132-
subtyping.
129+
However, structural subtyping can also be useful. For example, a "public API"
130+
may be more flexible if it is typed with protocols. Also, using protocol types
131+
removes the necessity to explicitly declare implementations of ABCs.
132+
As a rule of thumb, we recommend using nominal classes where possible, and
133+
protocols where necessary. For more details about protocol types and structural
134+
subtyping see :ref:`protocol-types` and
135+
`PEP 544 <https://www.python.org/dev/peps/pep-0544/>`_.
133136

134137
I like Python and I have no need for static typing
135138
**************************************************

docs/source/generics.rst

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Generics
22
========
33

4+
.. _generic-classes:
5+
46
Defining generic classes
57
************************
68

@@ -489,6 +491,73 @@ restrict the valid values for the type parameter in the same way.
489491
A type variable may not have both a value restriction (see
490492
:ref:`type-variable-value-restriction`) and an upper bound.
491493

494+
Generic protocols
495+
*****************
496+
497+
Generic protocols (see :ref:`protocol-types`) are also supported, generic
498+
protocols mostly follow the normal rules for generic classes, the main
499+
difference is that mypy checks that declared variance of type variables is
500+
compatible with the class definition. Examples:
501+
502+
.. code-block:: python
503+
504+
from typing import TypeVar
505+
from typing_extensions import Protocol
506+
507+
T = TypeVar('T')
508+
509+
class Box(Protocol[T]):
510+
content: T
511+
512+
def do_stuff(one: Box[str], other: Box[bytes]) -> None:
513+
...
514+
515+
class StringWrapper:
516+
def __init__(self, content: str) -> None:
517+
self.content = content
518+
519+
class BytesWrapper:
520+
def __init__(self, content: bytes) -> None:
521+
self.content = content
522+
523+
do_stuff(StringWrapper('one'), BytesWrapper(b'other')) # OK
524+
525+
x = None # type: Box[float]
526+
y = None # type: Box[int]
527+
x = y # Error, since the protocol 'Box' is invariant.
528+
529+
class AnotherBox(Protocol[T]): # Error, covariant type variable expected
530+
def content(self) -> T:
531+
...
532+
533+
T_co = TypeVar('T_co', covariant=True)
534+
class AnotherBox(Protocol[T_co]): # OK
535+
def content(self) -> T_co:
536+
...
537+
538+
ax = None # type: AnotherBox[float]
539+
ay = None # type: AnotherBox[int]
540+
ax = ay # OK for covariant protocols
541+
542+
See :ref:`variance-of-generics` above for more details on variance.
543+
Generic protocols can be recursive, for example:
544+
545+
.. code-block:: python
546+
547+
T = TypeVar('T')
548+
class Linked(Protocol[T]):
549+
val: T
550+
def next(self) -> 'Linked[T]': ...
551+
552+
class L:
553+
val: int
554+
def next(self) -> 'L': ...
555+
556+
def last(seq: Linked[T]) -> T:
557+
...
558+
559+
result = last(L()) # The inferred type of 'result' is 'int'
560+
492561
.. _declaring-decorators:
493562

494563
Declaring decorators

0 commit comments

Comments
 (0)