Skip to content

Commit 4186484

Browse files
committed
Update Post-History
Clarify that bare `ReadOnly` is not allowed, and add that to rejected ideas Specify that `type[Protocol]` does not inherit the protocol's attributes Close issue: "Extending Initialization" Bikeshed on some wording Use my real name
1 parent 1292ca3 commit 4186484

File tree

1 file changed

+42
-46
lines changed

1 file changed

+42
-46
lines changed

peps/pep-0767.rst

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
PEP: 767
22
Title: Annotating Read-Only Attributes
3-
Author: Eneg <eneg at discuss.python.org>
3+
Author: Łukasz Modzelewski <eneg at discuss.python.org>
44
Sponsor: Carl Meyer <carl@oddbird.net>
55
Discussions-To: https://discuss.python.org/t/pep-767-annotating-read-only-attributes/73408
66
Status: Draft
@@ -9,6 +9,7 @@ Topic: Typing
99
Created: 18-Nov-2024
1010
Python-Version: 3.15
1111
Post-History: `09-Oct-2024 <https://discuss.python.org/t/expanding-readonly-to-normal-classes-protocols/67359>`__
12+
`05-Dec-2024 <https://discuss.python.org/t/pep-767-annotating-read-only-attributes/73408>`__
1213

1314

1415
Abstract
@@ -58,7 +59,7 @@ Today, there are three major ways of achieving read-only attributes, honored by
5859
- Overriding ``number`` is not possible - the specification of ``Final``
5960
imposes that the name cannot be overridden in subclasses.
6061

61-
* read-only proxy via ``@property``::
62+
* marking the attribute "_internal", and exposing it via read-only ``@property``::
6263

6364
class Foo:
6465
_number: int
@@ -70,7 +71,7 @@ Today, there are three major ways of achieving read-only attributes, honored by
7071
def number(self) -> int:
7172
return self._number
7273

73-
- Overriding ``number`` is possible. *Type checkers disagree about the specific rules*. [#overriding_property]_
74+
- Overriding ``number`` is possible, but limited to using ``@property``. [#overriding_property]_
7475
- Read-only at runtime. [#runtime]_
7576
- Requires extra boilerplate.
7677
- Supported by :mod:`dataclasses`, but does not compose well - the synthesized
@@ -90,8 +91,7 @@ Today, there are three major ways of achieving read-only attributes, honored by
9091
- Read-only at runtime. [#runtime]_
9192
- No per-attribute control - these mechanisms apply to the whole class.
9293
- Frozen dataclasses incur some runtime overhead.
93-
- ``NamedTuple`` is still a ``tuple``. Most classes do not need to inherit
94-
indexing, iteration, or concatenation.
94+
- Most classes do not need indexing, iteration, or concatenation, inherited from ``NamedTuple``.
9595

9696
.. _protocols:
9797

@@ -123,8 +123,10 @@ This syntax has several drawbacks:
123123
* It is somewhat verbose.
124124
* It is not obvious that the quality conveyed here is the read-only character of a property.
125125
* It is not composable with :external+typing:term:`type qualifiers <type qualifier>`.
126-
* Not all type checkers agree [#property_in_protocol]_ that all of the above five
127-
objects are assignable to this structural type.
126+
* Currently, Pyright disagrees that some of the above five objects
127+
are assignable to this structural type.
128+
`[Pyright] <https://pyright-play.net/?pyrightVersion=1.1.404&pythonVersion=3.13&strict=true&code=GYJw9gtgBAhgRgYygSwgBzCALrOBnLEGBLCAUywAswATAKFEimAFcA7EsMAGzxXUw4ExSmRoB9NODRlsATwbhoWOWmRsA5vwzYoAYW4w8eAGowQAGigAFcFjAIeV4Opjc6HhIeNQAEkYAxLgAKWzB7R24ASgAuOigEqAABKTAZeXjEpPgCIhJyKlpMhJoyYGYQvDJuYCioAFoAPhQ2LBioADoujzoAYlhjZA02eGRuZBUeryM%2BILAAQSxCZDgWLDI4xIqwdvUsT29ZrjD0lU2s1NOFLdLy4Erq2obmvfaQChYQNigABgOZqBzAwzMwgc4Je47fSHUEAbT2AF0oABeX7-HxzAAiZDwCBAyDQ9jBxWSwgQogkl1kkxuZW2wSqNTqTRabSg7ywn2%2Bfzo0wxx2k1LkejAADdzMgYK1wckqRlaXcHkznlA4FxuG8Pl9AW4quijmAAJJscXjGgyyHtXL6qAAOTAcxlcHMVsIPTAcAAVu1-Hg5nQPZ6UYCuItlqt1sE6lB%2BmAANYBr3BuYnIVRxKxhOB5NcYHGUFbGNQeOJoOooEw8zphKZ0s5sDY3H4wmYdO17PlgVpIUi8X4qVYNvFrNJztGk1uZA0as1qCyEB11H2uYzwtF%2BeLu1gNhkNdr-objwHgAeaHGCAm2ncNrmYfxEbIhvQ3GCvrmsRJltZN67VyfZ9fQIuA-LYUkFeVEluelGSeFlXnZLVuR-MA81Mcx-xfN9gItLh2lQuFEWDHk%2BQNRs8QJIkMMAv1sJJJIyQpSRwJpSC6UhBlHmZF5pQQzltWIw4QzAVN5F7CUByorCwBAi5mOuVjFTADjlRZNUeE1PjvgCXUyGQ41TSnSSgOknCoWtOgkhcEZ3BIrc5iMmiTJJZ0wSga0gA>`_
129+
`[mypy] <https://mypy-play.net/?mypy=1.17.1&python=3.13&flags=strict&gist=12d556bb6ef4a9a49ff4ed4776604750>`_
128130

129131
Rationale
130132
=========
@@ -155,7 +157,7 @@ A class with a read-only instance attribute can now be defined as::
155157
return f"Hello, {obj.name}!"
156158

157159
* A subclass of ``Member`` can redefine ``.id`` as a writable attribute or a
158-
:term:`descriptor`. It can also :external+typing:term:`narrow` the type.
160+
:term:`descriptor`. It can also :external+typing:term:`narrow` its type.
159161
* The ``HasName`` protocol has a more succinct definition, and is agnostic
160162
to the writability of the attribute.
161163
* The ``greet`` function can now accept a wide variety of compatible objects,
@@ -167,7 +169,7 @@ Specification
167169

168170
The :external+py3.13:data:`typing.ReadOnly` :external+typing:term:`type qualifier`
169171
becomes a valid annotation for :term:`attributes <attribute>` of classes and protocols.
170-
It can be used at class-level or within ``__init__`` to mark individual attributes read-only::
172+
It can be used at class-level and within ``__init__`` to mark individual attributes read-only::
171173

172174
class Book:
173175
id: ReadOnly[int]
@@ -176,6 +178,7 @@ It can be used at class-level or within ``__init__`` to mark individual attribut
176178
self.id = id
177179
self.name: ReadOnly[str] = name
178180

181+
Use of bare ``ReadOnly`` (without ``[<type>]``) is not allowed.
179182
Type checkers should error on any attempt to reassign or ``del``\ ete an attribute
180183
annotated with ``ReadOnly``.
181184
Type checkers should also error on any attempt to delete an attribute annotated as ``Final``.
@@ -248,16 +251,16 @@ with ``ReadOnly`` is redundant, but it should not be seen as an error:
248251
Initialization
249252
--------------
250253

251-
Assignment to a read-only attribute can only occur in the class declaring the attribute.
254+
Assignment to a read-only attribute can only occur in the class declaring the attribute,
255+
at sites described below.
252256
There is no restriction to how many times the attribute can be assigned to.
253-
Depending on the kind of the attribute, they can be assigned to at different sites:
254257

255258
Instance Attributes
256259
'''''''''''''''''''
257260

258-
Assignment to an instance attribute must be allowed in the following contexts:
261+
Assignment to a read-only instance attribute must be allowed in the following contexts:
259262

260-
* In ``__init__``, on the instance received as the first parameter (likely, ``self``).
263+
* In ``__init__``, on the instance received as the first parameter (usually, ``self``).
261264
* In ``__new__``, on instances of the declaring class created via a call
262265
to a super-class' ``__new__`` method.
263266
* At declaration in the body of the class.
@@ -340,7 +343,7 @@ Read-only class attributes are attributes annotated as both ``ReadOnly`` and ``C
340343
Assignment to such attributes must be allowed in the following contexts:
341344

342345
* At declaration in the body of the class.
343-
* In ``__init_subclass__``, on the class object received as the first parameter (likely, ``cls``).
346+
* In ``__init_subclass__``, on the class object received as the first parameter (usually, ``cls``).
344347

345348
.. code-block:: python
346349
@@ -365,8 +368,8 @@ default for instances:
365368
self.number = number
366369
367370
.. note::
368-
This feature conflicts with :data:`~object.__slots__`. An attribute with
369-
a class-level value cannot be included in slots, effectively making it a class variable.
371+
This is possible only in classes without :data:`~object.__slots__`.
372+
An attribute included in slots cannot have a class-level default.
370373

371374
Type checkers may choose to warn on read-only attributes which could be left uninitialized
372375
after an instance is created (except in :external+typing:term:`stubs <stub>`,
@@ -464,8 +467,9 @@ This has a few subtyping implications. Borrowing from :pep:`705#inheritance`:
464467
def pprint(self) -> None:
465468
print(self.foo, self.bar, self.baz)
466469

467-
* In a protocol attribute declaration, ``name: ReadOnly[T]`` indicates that a structural
468-
subtype must support ``.name`` access, and the returned value is assignable to ``T``::
470+
* In a protocol attribute declaration, ``name: ReadOnly[T]`` indicates that values
471+
that inhabit the protocol must support ``.name`` access, and the returned value
472+
is assignable to ``T``::
469473

470474
class HasName(Protocol):
471475
name: ReadOnly[str]
@@ -493,6 +497,14 @@ This has a few subtyping implications. Borrowing from :pep:`705#inheritance`:
493497
has_name = NamedClassVar()
494498
has_name = NamedDescriptor()
495499

500+
Type checkers should not assume that access to a protocol's read-only attributes
501+
is supported by the protocol's type (``type[HasName]``).
502+
503+
Accurately modeling the behavior and type of ``type[HasName].name`` is difficult,
504+
therefore it was left out from this PEP to reduce its complexity;
505+
future enhancements to the typing specification may refine this behavior.
506+
507+
496508
Interaction with Other Type Qualifiers
497509
--------------------------------------
498510

@@ -596,50 +608,34 @@ since to allow assignment in ``__new__`` and classmethods under a set of rules
596608
described in the :ref:`init` section.
597609

598610

599-
Open Issues
600-
===========
611+
Allowing Bare ``ReadOnly`` With Initializing Value
612+
--------------------------------------------------
601613

602-
Extending Initialization
603-
------------------------
614+
An earlier version of this PEP allowed the use of bare ``ReadOnly`` when the attribute
615+
being annotated had an initializing value. The type of the attribute was supposed
616+
to be determined by type checkers using their usual type inference rules.
604617

605-
Mechanisms such as :func:`dataclasses.__post_init__` or attrs' `initialization hooks <https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization>`_
606-
augment object creation by providing a set of special hooks which are called
607-
during initialization.
618+
`This thread <https://github.com/python/peps/pull/4127#discussion_r1849261608>`_
619+
surfaced a few non-trivial issues with this feature, like undesirable inference
620+
of ``Literal[...]`` from literal values, differences in type checker inference rules,
621+
or complexity of implementation due to class-level and ``__init__``-level assignments.
622+
We decided to always require a type for ``ReadOnly[...]``, as *explicit is better than implicit*.
608623

609-
The current initialization rules defined in this PEP disallow assignment to
610-
read-only attributes in such methods. It is unclear whether the rules could be
611-
satisfyingly shaped in a way that is inclusive of those 3rd party hooks, while
612-
upkeeping the invariants associated with the read-only-ness of those attributes.
613-
614-
The Python type system has a long and detailed `specification <https://typing.python.org/en/latest/spec/constructors.html>`_
615-
regarding the behavior of ``__new__`` and ``__init__``. It is rather unfeasible
616-
to expect the same level of detail from 3rd party hooks.
617-
618-
A potential solution would involve type checkers providing configuration in this
619-
regard, requiring end users to manually specify a set of methods they wish
620-
to allow initialization in. This however could easily result in users mistakenly
621-
or purposefully breaking the aforementioned invariants. It is also a fairly
622-
big ask for a relatively niche feature.
623624

624625
Footnotes
625626
=========
626627

627628
.. [#overriding_property]
628629
Pyright in strict mode disallows non-property overrides.
629-
Mypy does not impose this restriction and allows an override with a plain attribute.
630+
Mypy permits an override with a plain attribute.
631+
Non-property overrides are technically unsafe, as they may break class-level ``Foo.number`` access.
630632
`[Pyright playground] <https://pyright-play.net/?strict=true&code=MYGwhgzhAEBiD28BcBYAUNT0D6A7ArgLYBGApgE5LQCWuALuultACakBmO2t1d22ACgikQ7ADTQCJClVp0AlNAC0APmgA5eLlKoMzLMNEA6PETLloAXklmKjPZgACAB3LxnFOgE8mWNpylzIRF2RVUael19LHJSOnxyXGhDdhNAuzR7UEgYACEwcgEEeHkorHTKCIY0IA>`_
631633
`[mypy playground] <https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=6f860a865c5d13cce07d6cbb08b9fb85>`_
632634
633635
.. [#runtime]
634636
This PEP focuses solely on the type-checking behavior. Nevertheless, it should
635637
be desirable the name is read-only at runtime.
636638
637-
.. [#property_in_protocol]
638-
Pyright disallows class variable and non-property descriptor overrides.
639-
`[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>`_
640-
`[mypy] <https://mypy-play.net/?mypy=1.13.0&python=3.12&flags=strict&gist=12d556bb6ef4a9a49ff4ed4776604750>`_
641-
`[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>`_
642-
643639
.. [#final_mutability]
644640
As noted above the second-to-last code example of https://typing.python.org/en/latest/spec/qualifiers.html#semantics-and-examples
645641

0 commit comments

Comments
 (0)