Skip to content

Conversation

@ilevkivskyi
Copy link
Member

This adds static support for structural subtyping. Previous discussion is here python/typing#11

Fixes #222

Copy link
Contributor

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments based on a quick read-through.

pep-0544.txt Outdated
provide sophisticated runtime instance and class checks against protocol
classes. This would be difficult and error-prone and will contradict the logic
of PEP 484. As well, following PEP 484 and PEP 526 we state that protocols are
**completely optional** and there is no intent to make them required.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little unclear what 'completely optional' means here and whether the following sentences are related to this. This could perhaps mean some of these things:

  • They are optional like everything defined in PEP 484: They don't affect Python runtime semantics and they are merely a feature in typing. Programmers are free to not use them.
  • The standard library doesn't use protocols outside typing.
  • Programmers are free to not use them even if they use type annotations.
  • Python implementations don't need to support them.

This could rephrased like this to avoid the ambiguity:

... are completely optional:

  • No runtime semantics will be imposed for variables or parameters annotated with a protocol class.
  • Any checks will be performed only by third-party type checkers and other tools.
  • ... maybe more

There is no intent to make protocols non-optional in the future.

a compatible type signature.


Protocol members
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to imply that sequences no longer support iteration, since Sequence.__iter__ is not abstract and thus is not a protocol member. Is this intentional?

My view is that protocols should not have non-protocol attributes or methods. For example, none of the ABCs in collections.abc seem to have what I'd consider non-protocol members. Protocols should describe an interface, not an implementation. Default implementations are okay, but they should probably only use other members defined in the protocol.

If we want to support non-protocol members, I think we should restrict them to names with a double underscore prefix to make this super explicit. However, even this brings in some arguably unneeded complexity: self will now have a special type, since accessing non-protocol members is only safe through self. Example:

class C(Protocol):
    ...
    def f(self, other: 'C') -> None:
        self.__g()  # ok
        other.__g()  # not ok

    def __g(self) -> None:
        <something>

Copy link
Member Author

@ilevkivskyi ilevkivskyi Mar 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to imply that sequences no longer support iteration, since Sequence.__iter__ is not abstract and thus is not a protocol member. Is this intentional?

Sequence implements __iter__ therefore it will be accepted wherever Iterable is expected. But asking for __iter__ on Sequence indeed will fail.

This indeed looks problematic. Here is a solution that I could propose: we do not have such thing as non-protocol members. However, there will be two kinds of members, abstract and non-abstract. All of them should be implemented in order to consider a class as structural (implicit) subtype.

The reason to have non-abstract methods is that one can get them "for free" using explicit subclassing provided abstract methods are implemented in subclass. So that for larger protocols like Sequence or Mapping one doesn't need to do

def method(self):
    return super().method()

for every method.

This coincides with how ABCs work at runtime, so that this will be intuitive for someone who already worked with ABCs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protocols should describe an interface, not an implementation.

👍

Default implementations are okay, but they should probably only use other members defined in the protocol.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a solution that I could propose: we do not have such thing as non-protocol members.

That's now specified below in the PEP right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's now specified below in the PEP right?

Yes.

pep-0544.txt Outdated
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]``.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unclear. What is PInt?

pep-0544.txt Outdated

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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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."

pep-0544.txt Outdated
finish(GoodJob()) # OK

In addition, we propose to add another special type construct
``All`` that represents intersection types. Although for normal types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider leaving out All from this proposal, at least initially. This is something we can add later if there is a need, but currently I don't see evidence for this being important. Effectively the same behavior can be achieved through multiple inheritance. Besides, it would be a bit arbitrary if All only worked with protocols, and making All work with arbitrary types would be a much more complex feature, and not as directly related to this PEP.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could include this for three reasons:

  • I could easily imagine a function parameter that requires more than one protocol, especially if the preferred style is to have several simple protocols, rather than few complex. By the way, some classes in collections.abc are simple intersections, for example Collection == All[Sized, Iterable, Container].
  • It could be easily implemented for protocols. I also propose to use All instead of Intersection to emphasize it only works for protocols (mnemonics: variable implements all these protocols). On the contrary, I think that intersections of nominal classes would be very rarely used.
  • If we decide to include this later, then people might simply be not aware of this feature (this is a minor reason, but still we need to take this into account).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.