11PEP: 767
22Title: Annotating Read-Only Attributes
3- Author: Eneg <eneg at discuss.python.org>
3+ Author: Łukasz Modzelewski <eneg at discuss.python.org>
44Sponsor: Carl Meyer <carl@oddbird.net>
55Discussions-To: https://discuss.python.org/t/pep-767-annotating-read-only-attributes/73408
66Status: Draft
@@ -9,6 +9,7 @@ Topic: Typing
99Created: 18-Nov-2024
1010Python-Version: 3.15
1111Post-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
1415Abstract
@@ -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
129131Rationale
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
168170The :external+py3.13:data: `typing.ReadOnly ` :external+typing:term: `type qualifier `
169171becomes 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.
179182Type checkers should error on any attempt to reassign or ``del ``\ ete an attribute
180183annotated with ``ReadOnly ``.
181184Type 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:
248251Initialization
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.
252256There 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
255258Instance 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
340343Assignment 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
371374Type checkers may choose to warn on read-only attributes which could be left uninitialized
372375after 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+
496508Interaction with Other Type Qualifiers
497509--------------------------------------
498510
@@ -596,50 +608,34 @@ since to allow assignment in ``__new__`` and classmethods under a set of rules
596608described 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
624625Footnotes
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