-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
PEP 544: Protocols #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PEP 544: Protocols #224
Changes from 1 commit
3fa3e48
51b0ca0
993f7b3
e384336
7ea5d41
cdcf62f
1ffed9b
72ceae6
6bea2e8
d5972c3
57d375f
9d4d685
82258d5
5d9fb7c
a6e6d9e
3175013
cbff669
dfccd06
60f4d52
60e7f7f
c90aa1c
b008de1
02cca5c
7d89b6b
0f3732a
95fbf58
cb65bff
817bf2f
2d89ba9
0efcbff
ebd4b17
0de36be
767c58b
efc3154
7d714c3
d4ab050
d9d21c2
4dfbfb2
d51420e
f6240c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -170,7 +170,8 @@ approaches related to structural subtyping in Python and other languages: | |
|
||
As mentioned in `rationale`_, we want to avoid such necessity, especially | ||
in static context. However, in runtime context, ABCs are good candidates for | ||
protocol classes and they are already used extensively in the ``typing`` module. | ||
protocol classes and they are already used extensively in | ||
the ``typing`` module. | ||
|
||
* Abstract classes defined in ``collections.abc`` module [collections-abc]_ | ||
are slightly more advanced since they implement a custom | ||
|
@@ -348,15 +349,15 @@ Having an empty method body in a protocol type makes the method | |
implicitly abstract only statically, and there won't be any runtime checking. | ||
The rationale for this is that most methods in a protocol will be abstract, | ||
and having to always use ``@abstractmethod`` is a little verbose and ugly. | ||
Leaving the method body empty is the recommended way of defining protocol members | ||
that don't have a default implementation. Such syntax mirrors how methods are | ||
defined in stub files. Abstract staticmethods and abstract classmethods | ||
are equally supported. | ||
Leaving the method body empty is the recommended way of defining protocol | ||
members that don't have a default implementation. Such syntax mirrors how | ||
methods are defined in stub files. Abstract staticmethods and abstract | ||
classmethods are equally supported. | ||
|
||
To define a protocol variable, one should use PEP 526 variable | ||
annotations in the class body. Attributes defined in the body of a method | ||
by assignment via ``self`` are considered non-protocol members. The rationale for | ||
this is that the protocol class implementation is often not shared by | ||
by assignment via ``self`` are considered non-protocol members. The rationale | ||
for this is that the protocol class implementation is often not shared by | ||
subtypes, so the interface should not depend on the default implementation. | ||
An exception from these rules is an attribute starting with double | ||
underscores and not ending with double underscores always considered | ||
|
@@ -422,8 +423,8 @@ can be used as regular base classes. In this case: | |
represent(BadColor()) # Error, 'add_int' is not implemented implicitly | ||
|
||
* Type checkers will statically enforce that the class | ||
actually implements the protocol correctly. In particular, type checkers will | ||
always require members without default implementation to be defined in | ||
actually implements the protocol correctly. In particular, type checkers | ||
will always require members without default implementation to be defined in | ||
subclasses that explicitly subclass the protocol:: | ||
|
||
from typing import Protocol, Tuple | ||
|
@@ -447,9 +448,10 @@ can be used as regular base classes. In this case: | |
The general philosophy is that protocols are mostly like regular ABCs, | ||
but a static type checker will handle them specially. | ||
In particular, a class can explicitly inherit from multiple protocols. | ||
In this case methods are resolved using normal MRO and the type checker verifies | ||
that all subtyping are correct. Note that a concrete method with appropriate | ||
type signature can override a protocol method but not vice versa. Examples:: | ||
In this case methods are resolved using normal MRO and the type checker | ||
verifies that all subtyping are correct. Note that a concrete method with | ||
appropriate type signature can override a protocol method but not vice versa. | ||
Examples:: | ||
|
||
class A(Protocol): | ||
def m(self) -> None: ... | ||
|
@@ -559,8 +561,8 @@ relationships are subject to the following rules: | |
|
||
Generic protocol types follow the same rules of variance as non-protocol | ||
types. Protocols can be used in all special type constructors provided | ||
by the ``typing`` module and follow the corresponding subtyping rules. For example, | ||
protocol ``PInt`` is a subtype of ``Union[PInt, int]``. | ||
by the ``typing`` module and follow the corresponding subtyping rules. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be rephrased as something like "Protocol types can be used in all contexts where any other types can used, such as in (examples). Generic protocols follow the rules for generic abstract classes, except for using structural compatibility instead of compatibility defined by inheritance relationships." |
||
For example, protocol ``PInt`` is a subtype of ``Union[PInt, int]``. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unclear. What is |
||
|
||
|
||
``Union[]`` and ``All[]`` | ||
|
@@ -746,10 +748,10 @@ runtime, which is unfortunate:: | |
print(b.x) # 1 | ||
print(B.x) # Ellipsis | ||
|
||
For this reason we propose to make the ``Protocol`` metaclass recognize ``...`` | ||
initializers and translate them to a special property that can't be read. | ||
The ``typing`` module changes proposed in this PEP will be also backported to | ||
earlier versions via the backport currently available on PyPI. | ||
For this reason we propose to make the ``Protocol`` metaclass recognize | ||
``...`` initializers and translate them to a special property that can't | ||
be read. The ``typing`` module changes proposed in this PEP will be also | ||
backported to earlier versions via the backport currently available on PyPI. | ||
|
||
|
||
Runtime Implementation of Protocol Classes | ||
|
@@ -818,9 +820,9 @@ Introspection | |
|
||
The existing class introspection machinery (``dir``, ``__annotations__`` etc) | ||
can be used with protocols. In addition, all introspection tools implemented | ||
in the ``typing`` module will support protocols. Since all attributes need to be | ||
defined in the class body based on this proposal, protocol classes will have | ||
even better perspective for introspection than regular classes where | ||
in the ``typing`` module will support protocols. Since all attributes need | ||
to be defined in the class body based on this proposal, protocol classes will | ||
have even better perspective for introspection than regular classes where | ||
attributes can be defined implicitly -- protocol attributes can't be | ||
initialized in ways that are not visible to introspection | ||
(using ``setattr()``, assignment via ``self``, etc.). Still, some things like | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well in some sense that same rationale would apply to default (i.e. non-abstract) implementations.
A defensible position could be that as long as a type checker can verify that an implementation that doesn't explicitly inherit from the protocol class still defines the variable, there's no great reason to disallow variables, since they could just be interpreted as a shorthand for a setter and a getter method.
Also note that mypy (to take one example) doesn't actually check whether an attribute is always set -- it only checks that if it is used it has the right type.
All this gets pretty messy though and I am leaning towards not allowing protocol variables at all, other than read-only properties (possibly abstract). I don't recall having seen real-world examples of them (but if there are examples that might sway me).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this point needs some clarifications. First, I don't see why we need to force people to write:
instead of just
I think almost every function parameter annotated with a named tuple or a similar struct-like class could be replaced with a protocol annotation. (Also, this will be a natural counterpart of
TypedDict
.) Second, I don't think we need to specify how "smart" type checkers should be. Maybe we could only mention that it is reasonable to expect that a type checker will recognizeas implicitly implementing
Point2D
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disallowing protocol variables entirely significantly limits the scope of structural typing. There's many examples in the stdlib alone (like the
stdout.buffer
field, named tuples and enums used structurally, etc.).Only allowing read-only properties is less limiting but still so. More importantly, it is confusing to the user, like Ivan's example shows above:
I can imagine users of this rewriting their attributes to be properties just to satisfy the protocol specification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not adamantly against protocol variables, but I think we should at least settle what to do about read-only attributes. When you use
@property
(not using the@f.setter
syntax) or use PEP 526 syntax inside aNamedTuple
you get a read-only attribute; otherwise PEP 526 syntax gets you a writable attribute. Which should be the default for Protocols, and how should you be able to declare the other kind?Note that this is not about immutability -- that's in the programmer's head (or perhaps guided by whether
__hash__
is defined). This is purely about whether attribute assignment should be allowed.