Skip to content

Commit

Permalink
Rewrite the Protocols section
Browse files Browse the repository at this point in the history
  • Loading branch information
Enegg committed Nov 22, 2024
1 parent df4ddc3 commit 15b4ed6
Showing 1 changed file with 18 additions and 23 deletions.
41 changes: 18 additions & 23 deletions peps/pep-0767.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,39 +98,32 @@ Today, there are three major ways of achieving read-only attributes, honored by
Protocols
---------

Paraphrasing `this post <https://github.com/python/typing/discussions/1525>`_,
there is no way of defining an attribute ``name: T`` on a :class:`~typing.Protocol`,
such that the only requirements to satisfy are:
A read-only structural attribute ``name: T`` on a :class:`~typing.Protocol` in principle
defines the following two requirements:

1. ``hasattr(obj, "name")``
2. ``isinstance(obj.name, T)`` [#invalid_typevar]_
2. ``isinstance(obj.name, T)``

The above are satisfiable at runtime by all of the following:
Those requirements are satisfiable at runtime by all of the following:

1. an object with an attribute ``name: T``,
2. a class with a class variable ``name: ClassVar[T]``, [#invalid_typevar]_
2. a class with a class variable ``name: ClassVar[T]``,
3. an instance of the class above,
4. an object with a ``@property`` ``def name(self) -> T``,
5. an object with a custom descriptor, such as :func:`functools.cached_property`.

Note that the attribute being marked ``Final`` or the property defining a setter
do not impact this.

The most common practice is to define such a protocol with a ``@property``::
The current `typing spec <https://typing.readthedocs.io/en/latest/spec/protocol.html#protocol-members>`_
defines that read-only protocol variables can be created using (abstract) properties::

class HasName(Protocol):
@property
def name(self) -> T: ...

Type checkers special-case this definition, such that objects with plain attributes
are assignable to the type. However, instances with class variables and descriptors
other than ``property`` are rejected.

Covering the extra possibilities induces a great amount of boilerplate, involving
creation of an abstract descriptor protocol, possibly also accounting for
class and instance level overloads.
Worse yet, all of that is multiplied for each additional read-only attribute.

- The syntax is somewhat verbose.
- It is not obvious that the quality conveyed here is the read-only character of a property.
- Not composable with ``ClassVar`` or ``Annotated``.
- Not all type checkers agree [#property_in_protocol]_ that all of the above five
objects are assignable to this structural type.

Rationale
=========
Expand Down Expand Up @@ -162,7 +155,7 @@ A class with a read-only instance attribute can now be defined as::

* A subclass of ``Member`` can redefine ``.id`` as a writable attribute or a
:term:`descriptor`. It can also :external+typing:term:`narrow` the type.
* The ``HasName`` protocol can be implemented by any mechanism allowing for ``.name`` access.
* The ``HasName`` protocol succinctly expresses operations available on its attribute.
* The ``greet`` function can now accept a wide variety of compatible objects,
while being explicit about no modifications being done to the input.

Expand Down Expand Up @@ -538,9 +531,11 @@ Footnotes
This PEP focuses solely on the type-checking behavior. Nevertheless, it should
be desirable the name is read-only at runtime.
.. [#invalid_typevar]
The implied type variable is not valid in this context; it has been used for
the ease of demonstration. See `ClassVar <https://typing.readthedocs.io/en/latest/spec/class-compat.html#classvar>`_.
.. [#property_in_protocol]
Pyright disallows class variable and non-property descriptor overrides.
`[Pyright] <https://pyright-play.net/?pyrightVersion=1.1.389&pythonVersion=3.13&strict=true&code=GYJw9gtgBAhgRgYygSwgBzCALrOBnLEGBLCAUywAswATAKFEimAFcA7EsMAGzxXUw4ExSmRoB9NODRlsATwbhoWOWmRsA5vwzYoAYW4w8eAGowQAGigAFcFjAIeV4Opjc6HhIeNQAEkYAxLgAKWzB7R24ASgAuOigEqAABKTAZeXjEpPgCIhJyKlpMhJoyYGYQvDJuYCioAFoAPhQ2LBioADoujzoAYlhjZA02eGRuZBUeryM%2BILAAQSxCZDgWLDI4xIqwdvUsT29ZrjD0lU2s1NOFLdLy4Erq2obmvfaQChYQNigABgOZqBzAwzMwgc4Je47fSHUEAbT2AF0oABeX7-HxzAAiZDwCBAyDQ9jBxWSwgQogkl1kkxuZW2wSqNTqTRabSg7ywn2%2Bfzo0wxx2k1LkejAADdzMgYK1wckqRlaXcHkznlA4FxuG8Pl9AW4quijmAAJJscXjGgyyHtXL6qAAOTAcxlcHMVsIPTAcAAVu1-Hg5nQPZ6UYCuItlqt1sE6lB%2BmAANYBr3BuYnIVRxKxhOB5NcYHGUFbGNQeOJoOooEw8zphKZ0s5sDY3H4wmYdO17PlgVpIUi8X4qVYNvFrNJztGk1uZA0as1qCyEB11H2uYzwtF%2BeLu1gNhkNdr-objwHgAeaHGCAm2ncNrmYfxEbIhvQ3GCvrmsRJltZN67VyfZ9fQIuA-LYUkFeVEluelGSeFlXnZLVuR-MA81Mcx-xfN9gItLh2lQuFEWDHk%2BQNRs8QJIkMMAv1sJJJIyQpSRwJpSC6UhBlHmZF5pQQzltWIw4QzAVN5F7CUByorCwBAi5mOuVjFTADjlRZNUeE1PjvgCXUyGQ41TSnSSgOknCoWtOgkhcEZ3BIrc5iMmiTJJZ0wSga0gA>`_
`[mypy] <https://mypy-play.net/?mypy=1.13.0&python=3.12&flags=strict&gist=12d556bb6ef4a9a49ff4ed4776604750>`_
`[Pyre] <https://pyre-check.org/play/?input=%23%20pyre-strict%0Afrom%20abc%20import%20abstractmethod%0Afrom%20functools%20import%20cached_property%0Afrom%20typing%20import%20ClassVar%2C%20Protocol%2C%20final%0A%0A%0Aclass%20HasFoo(Protocol)%3A%0A%20%20%20%20%40property%0A%20%20%20%20%40abstractmethod%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20...%0A%0A%0A%23%20assignability%0A%0A%0Aclass%20FooAttribute%3A%0A%20%20%20%20foo%3A%20int%0A%0Aclass%20FooProperty%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooClassVar%3A%0A%20%20%20%20foo%3A%20ClassVar%5Bint%5D%20%3D%200%0A%0Aclass%20FooDescriptor%3A%0A%20%20%20%20%40cached_property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooPropertyCovariant%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20bool%3A%20return%20False%0A%0Aclass%20FooInvalid%3A%0A%20%20%20%20foo%3A%20str%0A%0Aclass%20NoFoo%3A%0A%20%20%20%20bar%3A%20str%0A%0A%0Aobj%3A%20HasFoo%0Aobj%20%3D%20FooAttribute()%20%20%23%20ok%0Aobj%20%3D%20FooProperty()%20%20%20%23%20ok%0Aobj%20%3D%20FooClassVar%20%20%20%20%20%23%20ok%0Aobj%20%3D%20FooClassVar()%20%20%20%23%20ok%0Aobj%20%3D%20FooDescriptor()%20%23%20ok%0Aobj%20%3D%20FooPropertyCovariant()%20%23%20ok%0Aobj%20%3D%20FooInvalid()%20%20%20%20%23%20err%0Aobj%20%3D%20NoFoo()%20%20%20%20%20%20%20%20%20%23%20err%0Aobj%20%3D%20None%20%20%20%20%20%20%20%20%20%20%20%20%23%20err%0A%0A%0A%23%20explicit%20impl%0A%0A%0Aclass%20FooAttributeImpl(HasFoo)%3A%0A%20%20%20%20foo%3A%20int%0A%0Aclass%20FooPropertyImpl(HasFoo)%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooClassVarImpl(HasFoo)%3A%0A%20%20%20%20foo%3A%20ClassVar%5Bint%5D%20%3D%200%0A%0Aclass%20FooDescriptorImpl(HasFoo)%3A%0A%20%20%20%20%40cached_property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooPropertyCovariantImpl(HasFoo)%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20bool%3A%20return%20False%0A%0Aclass%20FooInvalidImpl(HasFoo)%3A%0A%20%20%20%20foo%3A%20str%0A%0A%40final%0Aclass%20NoFooImpl(HasFoo)%3A%0A%20%20%20%20bar%3A%20str%0A>`_
.. [#final_mutability]
As noted above the second-to-last code example of https://typing.readthedocs.io/en/latest/spec/qualifiers.html#semantics-and-examples
Expand Down

0 comments on commit 15b4ed6

Please sign in to comment.