@@ -30,10 +30,11 @@ Motivation
3030
3131The Python type system lacks a single concise way to mark an attribute read-only.
3232This feature is present in other statically and gradually typed languages
33- (such as `C# <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly >`_
34- or `TypeScript <https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties >`_),
35- and is useful for removing the ability to reassign or ``del ``\ ete an attribute
36- at a type checker level, as well as defining a broad interface for structural subtyping.
33+ (such as `C# <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly >`__
34+ or `TypeScript <https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties >`__),
35+ and is useful for removing the ability to externally assign to or ``del ``\ ete
36+ an attribute at a type checker level, as well as defining a broad interface
37+ for structural subtyping.
3738
3839.. _classes :
3940
@@ -167,6 +168,9 @@ A class with a read-only instance attribute can now be defined as::
167168Specification
168169=============
169170
171+ Usage
172+ -----
173+
170174The :external+py3.13:data: `typing.ReadOnly ` :external+typing:term: `type qualifier `
171175becomes a valid annotation for :term: `attributes <attribute> ` of classes and protocols.
172176It can be used at class-level and within ``__init__ `` to mark individual attributes read-only::
@@ -179,100 +183,13 @@ It can be used at class-level and within ``__init__`` to mark individual attribu
179183 self.name: ReadOnly[str] = name
180184
181185Use of bare ``ReadOnly `` (without ``[<type>] ``) is not allowed.
182- Type checkers should error on any attempt to reassign or ``del ``\ ete an attribute
183- annotated with ``ReadOnly ``.
184- Type checkers should also error on any attempt to delete an attribute annotated as ``Final ``.
185- (This is not currently specified.)
186-
187- Use of ``ReadOnly `` in annotations at other sites where it currently has no meaning
188- (such as local/global variables or function parameters) is considered out of scope
189- for this PEP.
190-
191- Akin to ``Final `` [#final_mutability ]_, ``ReadOnly `` does not influence how
192- type checkers perceive the mutability of the assigned object. Immutable :term: `ABCs <abstract base class> `
193- and :mod: `containers <collections.abc> ` may be used in combination with ``ReadOnly ``
194- to forbid mutation of such values at a type checker level:
195-
196- .. code-block :: python
197-
198- from collections import abc
199- from dataclasses import dataclass
200- from typing import Protocol, ReadOnly
201-
202-
203- @dataclass
204- class Game :
205- name: str
206-
207-
208- class HasGames[T: abc.Collection[Game]](Protocol):
209- games: ReadOnly[T]
210-
211-
212- def add_games (shelf : HasGames[list[Game]]) -> None :
213- shelf.games.append(Game(" Half-Life" )) # ok: list is mutable
214- shelf.games[- 1 ].name = " Black Mesa" # ok: "name" is not read-only
215- shelf.games = [] # error: "games" is read-only
216- del shelf.games # error: "games" is read-only and cannot be deleted
217-
218-
219- def read_games (shelf : HasGames[abc.Sequence[Game]]) -> None :
220- shelf.games.append(... ) # error: "Sequence" has no attribute "append"
221- shelf.games[0 ].name = " Blue Shift" # ok: "name" is not read-only
222- shelf.games = [] # error: "games" is read-only
223-
224-
225- All instance attributes of frozen dataclasses and ``NamedTuple `` should be
226- implied to be read-only. Type checkers may inform that annotating such attributes
227- with ``ReadOnly `` is redundant, but it should not be seen as an error:
228-
229- .. code-block :: python
230-
231- from dataclasses import dataclass
232- from typing import NewType, ReadOnly
233-
234-
235- @dataclass (frozen = True )
236- class Point :
237- x: int # implicit read-only
238- y: ReadOnly[int ] # ok, redundant
239186
187+ Type checkers should error on any attempt to *externally mutate * an attribute
188+ annotated with ``ReadOnly ``.
240189
241- uint = NewType(" uint" , int )
242-
243-
244- @dataclass (frozen = True )
245- class UnsignedPoint (Point ):
246- x: ReadOnly[uint] # ok, redundant; narrower type
247- y: Final[uint] # not redundant, Final imposes extra restrictions; narrower type
248-
249- .. _init :
250-
251- Initialization
252- --------------
253-
254- Assignment to a read-only attribute can only occur in the class declaring the attribute,
255- at sites described below.
256- There is no restriction to how many times the attribute can be assigned to.
257-
258- Instance Attributes
259- '''''''''''''''''''
260-
261- Assignment to a read-only instance attribute must be allowed in the following contexts:
262-
263- * In ``__init__ ``, on the instance received as the first parameter (usually, ``self ``).
264- * In ``__new__ ``, on instances of the declaring class created via a call
265- to a super-class' ``__new__ `` method.
266- * At declaration in the body of the class.
267-
268- Additionally, a type checker may choose to allow the assignment:
269-
270- * In ``__new__ ``, on instances of the declaring class, without regard
271- to the origin of the instance.
272- (This choice trades soundness, as the instance may already be initialized,
273- for the simplicity of implementation.)
274- * In ``@classmethod ``\ s, on instances of the declaring class created via
275- a call to the class' or super-class' ``__new__ `` method.
190+ We define "externally" here as occurring outside the body of the class declaring
191+ the attribute, or its subclasses.
192+ "Mutate" means to assign to or ``del ``\ ete the attribute.
276193
277194.. code-block :: python
278195
@@ -292,8 +209,7 @@ Additionally, a type checker may choose to allow the assignment:
292209 self .songs = list (songs) # multiple assignments are fine
293210
294211 def clear (self ) -> None :
295- # error: assignment to read-only "songs" outside initialization
296- self .songs = []
212+ self .songs = [] # ok
297213
298214
299215 band = Band(name = " Bôa" , songs = [" Duvet" ])
@@ -302,10 +218,6 @@ Additionally, a type checker may choose to allow the assignment:
302218 band.songs.append(" Twilight" ) # ok: list is mutable
303219
304220
305- class SubBand (Band ):
306- def __init__ (self ) -> None :
307- self .songs = [] # error: cannot assign to a read-only attribute of a base class
308-
309221 .. code-block :: python
310222
311223 # a simplified immutable Fraction class
@@ -336,61 +248,76 @@ Additionally, a type checker may choose to allow the assignment:
336248 self .numerator, self .denominator = f.as_integer_ratio()
337249 return self
338250
339- Class Attributes
340- ''''''''''''''''
341251
342- Read-only class attributes are attributes annotated as both `` ReadOnly `` and `` ClassVar ``.
343- Assignment to such attributes must be allowed in the following contexts:
252+ It should also be error to delete an attribute annotated as `` Final ``.
253+ (This is not currently specified.)
344254
345- * At declaration in the body of the class.
346- * In ``__init_subclass__ ``, on the class object received as the first parameter (usually, ``cls ``).
255+ Use of ``ReadOnly `` in annotations at other sites where it currently has no meaning
256+ (such as local/global variables or function parameters) is considered out of scope
257+ for this PEP.
258+
259+ ``ReadOnly `` does not influence the mutability of the attribute's value. Immutable
260+ protocols and :mod: `collections <collections.abc> ` may be used in combination
261+ with ``ReadOnly `` to forbid mutation of those values at a type checker level:
347262
348263.. code-block :: python
349264
350- class URI :
351- protocol: ReadOnly[ClassVar[str ]] = " "
265+ from collections import abc
266+ from dataclasses import dataclass
267+ from typing import Protocol, ReadOnly
352268
353- def __init_subclass__ (cls , protocol : str = " " ) -> None :
354- cls .protocol = protocol
355269
356- class File (URI , protocol = " file" ): ...
270+ @dataclass
271+ class Game :
272+ name: str
357273
358- When a class-level declaration has an initializing value, it can serve as a `flyweight <https://en.wikipedia.org/wiki/Flyweight_pattern >`_
359- default for instances:
274+
275+ class HasGames[T: abc.Collection[Game]](Protocol):
276+ games: ReadOnly[T]
277+
278+
279+ def add_games (shelf : HasGames[list[Game]]) -> None :
280+ shelf.games.append(Game(" Half-Life" )) # ok: list is mutable
281+ shelf.games[- 1 ].name = " Black Mesa" # ok: "name" is not read-only
282+ shelf.games = [] # error: "games" is read-only
283+ del shelf.games # error: "games" is read-only and cannot be deleted
284+
285+
286+ def read_games (shelf : HasGames[abc.Sequence[Game]]) -> None :
287+ # shelf.games.append(...) error, "Sequence" has no "append"!
288+ shelf.games[0 ].name = " Blue Shift" # ok: "name" is not read-only
289+ shelf.games = [] # error: "games" is read-only
290+
291+
292+ All instance attributes of frozen dataclasses and ``NamedTuple `` should be
293+ implied to be read-only. Type checkers may inform that annotating such attributes
294+ with ``ReadOnly `` is redundant, but it should not be seen as an error:
360295
361296.. code-block :: python
362297
363- class Patient :
364- number: ReadOnly[ int ] = 0
298+ from dataclasses import dataclass
299+ from typing import NewType, ReadOnly
365300
366- def __init__ (self , number : int | None = None ) -> None :
367- if number is not None :
368- self .number = number
369301
370- .. note ::
371- This is possible only in classes without :data: `~object.__slots__ `.
372- An attribute included in slots cannot have a class-level default.
302+ @dataclass (frozen = True )
303+ class Point :
304+ x: int # implicit read-only
305+ y: ReadOnly[int ] # ok, redundant
373306
374- Type checkers may choose to warn on read-only attributes which could be left uninitialized
375- after an instance is created (except in :external+typing:term: `stubs <stub> `,
376- protocols or ABCs)::
377307
378- class Patient:
379- id: ReadOnly[int] # error: "id" is not initialized on all code paths
380- name: ReadOnly[str] # error: "name" is never initialized
308+ uint = NewType(" uint" , int )
381309
382- def __init__(self) -> None:
383- if random.random() > 0.5:
384- self.id = 123
385310
311+ @dataclass (frozen = True )
312+ class UnsignedPoint (Point ):
313+ x: ReadOnly[uint] # ok, redundant; narrower type
314+ y: Final[uint] # not redundant, Final imposes extra restrictions; narrower type
386315
387- class HasName(Protocol):
388- name: ReadOnly[str] # ok
389316
390317 Subtyping
391318---------
392319
393- The inability to reassign read-only attributes makes them covariant.
320+ The inability to externally mutate read-only attributes makes them covariant.
394321This has a few subtyping implications. Borrowing from :pep: `705#inheritance `:
395322
396323* Read-only attributes can be redeclared as writable attributes, descriptors
@@ -409,7 +336,7 @@ This has a few subtyping implications. Borrowing from :pep:`705#inheritance`:
409336
410337 game = Game(title="DOOM", year=1993)
411338 game.year = 1994
412- game.title = "DOOM II" # ok: attribute is not read-only
339+ game.title = "DOOM II" # ok: attribute is no longer read-only
413340
414341
415342 class TitleProxy(HasTitle):
@@ -422,17 +349,13 @@ This has a few subtyping implications. Borrowing from :pep:`705#inheritance`:
422349
423350* If a read-only attribute is not redeclared, it remains read-only::
424351
352+ @dataclass
425353 class Game(HasTitle):
426354 year: int
427355
428- def __init__(self, title: str, year: int) -> None:
429- super().__init__(title)
430- self.title = title # error: cannot assign to a read-only attribute of base class
431- self.year = year
432-
433-
434356 game = Game(title="Robot Wants Kitty", year=2010)
435357 game.title = "Robot Wants Puppy" # error: "title" is read-only
358+ game.year = 2012 # ok
436359
437360* Subtypes can :external+typing:term: `narrow ` the type of read-only attributes::
438361
@@ -526,10 +449,25 @@ Interaction with Other Type Qualifiers
526449 This is consistent with the interaction of ``ReadOnly `` and :class: `typing.TypedDict `
527450defined in :pep: `705 `.
528451
529- An attribute cannot be annotated as both ``ReadOnly `` and ``Final ``, as the two
530- qualifiers differ in semantics, and ``Final `` is generally more restrictive.
531- ``Final `` remains allowed as an annotation of attributes that are only implied
532- to be read-only. It can be also used to redeclare a ``ReadOnly `` attribute of a base class.
452+ Read-only class attributes can be *internally * assigned to in the same places
453+ a normal class variable can:
454+
455+ .. code-block :: python
456+
457+ class URI :
458+ protocol: ReadOnly[ClassVar[str ]] = " "
459+
460+ def __init_subclass__ (cls , protocol : str = " " ) -> None :
461+ cls .protocol = protocol
462+
463+ class File (URI , protocol = " file" ): ...
464+
465+ URI .protocol = " http" # error: "protocol" is read-only
466+
467+ ``Final `` attributes are implicitly read-only. Annotating an attribute as both
468+ ``Final `` and ``ReadOnly `` is redundant and should be flagged as such by type checkers.
469+ ``Final `` may be used to override both implicit and explicit read-only attributes
470+ of a base class.
533471
534472
535473Backwards Compatibility
@@ -570,8 +508,8 @@ following the footsteps of :pep:`705#how-to-teach-this`:
570508 `type qualifiers <https://typing.python.org/en/latest/spec/qualifiers.html >`_ section:
571509
572510 The ``ReadOnly `` type qualifier in class attribute annotations indicates
573- that the attribute of the class may be read, but not reassigned or `` del `` \ eted.
574- For usage in ``TypedDict ``, see `ReadOnly <https://typing.python.org/en/latest/spec/typeddict.html#typing-readonly-type-qualifier >`_.
511+ that outside of the class, the attribute may be read but not assigned to
512+ or `` del `` \ eted. For usage in ``TypedDict ``, see `ReadOnly <https://typing.python.org/en/latest/spec/typeddict.html#typing-readonly-type-qualifier >`_.
575513
576514
577515Rejected Ideas
@@ -588,26 +526,22 @@ quality of such properties.
588526This PEP makes ``ReadOnly `` a better alternative for defining read-only attributes
589527in protocols, superseding the use of properties for this purpose.
590528
529+ Assignment Only in ``__init__ `` and Class Scope
530+ -----------------------------------------------
591531
592- Assignment Only in ``__init__ `` and Class Body
593- ----------------------------------------------
594-
595- An earlier version of this PEP proposed that read-only attributes could only be
596- assigned to in ``__init__ `` and the class' body. A later discussion revealed that
597- this restriction would severely limit the usability of ``ReadOnly `` within
598- immutable classes, which typically do not define ``__init__ ``.
532+ An earlier version of this PEP specified that internal mutation of read-only
533+ attributes could only happen in ``__init__ `` and at class-level. This was done
534+ to follow suit the specification of C#'s `readonly <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly >`__.
599535
600- :class: `fractions.Fraction ` is one example of an immutable class, where the
601- initialization of its attributes happens within ``__new__ `` and classmethods.
602- However, unlike in ``__init__ ``, the assignment in ``__new__ `` and classmethods
603- is potentially unsound, as the instance they work on can be sourced from
604- an arbitrary place, including an already finalized instance.
605-
606- We find it imperative that this type checking feature is useful to the foremost
607- use site of read-only attributes - immutable classes. Thus, the PEP has changed
608- since to allow assignment in ``__new__ `` and classmethods under a set of rules
609- described in the :ref: `init ` section.
536+ Later revision of this PEP loosened the restriction to also include ``__new__ ``,
537+ ``__init_subclass__ `` and ``@classmethod ``\ s, as it was revealed that the initial
538+ version would severely limit the usability of ``ReadOnly `` within immutable classes,
539+ which typically do not define ``__init__ ``.
610540
541+ Further revision removed this restriction entirely, as it turned out unnecessary
542+ to achieve soundness of the effects of ``ReadOnly `` as described in this PEP.
543+ In turn, this allowed to simplify the PEP, and should reduce the complexity
544+ of type checker implementations.
611545
612546Allowing Bare ``ReadOnly `` With Initializing Value
613547--------------------------------------------------
@@ -637,9 +571,6 @@ Footnotes
637571 This PEP focuses solely on the type-checking behavior. Nevertheless, it should
638572 be desirable the name is read-only at runtime.
639573
640- .. [#final_mutability ]
641- As noted above the second-to-last code example of https://typing.python.org/en/latest/spec/qualifiers.html#semantics-and-examples
642-
643574
644575 Copyright
645576=========
0 commit comments