@@ -30,10 +30,11 @@ Motivation
30
30
31
31
The Python type system lacks a single concise way to mark an attribute read-only.
32
32
This 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.
37
38
38
39
.. _classes :
39
40
@@ -167,6 +168,9 @@ A class with a read-only instance attribute can now be defined as::
167
168
Specification
168
169
=============
169
170
171
+ Usage
172
+ -----
173
+
170
174
The :external+py3.13:data: `typing.ReadOnly ` :external+typing:term: `type qualifier `
171
175
becomes a valid annotation for :term: `attributes <attribute> ` of classes and protocols.
172
176
It 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
179
183
self.name: ReadOnly[str] = name
180
184
181
185
Use 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
239
186
187
+ Type checkers should error on any attempt to *externally mutate * an attribute
188
+ annotated with ``ReadOnly ``.
240
189
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.
276
193
277
194
.. code-block :: python
278
195
@@ -292,8 +209,7 @@ Additionally, a type checker may choose to allow the assignment:
292
209
self .songs = list (songs) # multiple assignments are fine
293
210
294
211
def clear (self ) -> None :
295
- # error: assignment to read-only "songs" outside initialization
296
- self .songs = []
212
+ self .songs = [] # ok
297
213
298
214
299
215
band = Band(name = " Bôa" , songs = [" Duvet" ])
@@ -302,10 +218,6 @@ Additionally, a type checker may choose to allow the assignment:
302
218
band.songs.append(" Twilight" ) # ok: list is mutable
303
219
304
220
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
-
309
221
.. code-block :: python
310
222
311
223
# a simplified immutable Fraction class
@@ -336,61 +248,76 @@ Additionally, a type checker may choose to allow the assignment:
336
248
self .numerator, self .denominator = f.as_integer_ratio()
337
249
return self
338
250
339
- Class Attributes
340
- ''''''''''''''''
341
251
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.)
344
254
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:
347
262
348
263
.. code-block :: python
349
264
350
- class URI :
351
- protocol: ReadOnly[ClassVar[str ]] = " "
265
+ from collections import abc
266
+ from dataclasses import dataclass
267
+ from typing import Protocol, ReadOnly
352
268
353
- def __init_subclass__ (cls , protocol : str = " " ) -> None :
354
- cls .protocol = protocol
355
269
356
- class File (URI , protocol = " file" ): ...
270
+ @dataclass
271
+ class Game :
272
+ name: str
357
273
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:
360
295
361
296
.. code-block :: python
362
297
363
- class Patient :
364
- number: ReadOnly[ int ] = 0
298
+ from dataclasses import dataclass
299
+ from typing import NewType, ReadOnly
365
300
366
- def __init__ (self , number : int | None = None ) -> None :
367
- if number is not None :
368
- self .number = number
369
301
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
373
306
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)::
377
307
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 )
381
309
382
- def __init__(self) -> None:
383
- if random.random() > 0.5:
384
- self.id = 123
385
310
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
386
315
387
- class HasName(Protocol):
388
- name: ReadOnly[str] # ok
389
316
390
317
Subtyping
391
318
---------
392
319
393
- The inability to reassign read-only attributes makes them covariant.
320
+ The inability to externally mutate read-only attributes makes them covariant.
394
321
This has a few subtyping implications. Borrowing from :pep: `705#inheritance `:
395
322
396
323
* 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`:
409
336
410
337
game = Game(title="DOOM", year=1993)
411
338
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
413
340
414
341
415
342
class TitleProxy(HasTitle):
@@ -422,17 +349,13 @@ This has a few subtyping implications. Borrowing from :pep:`705#inheritance`:
422
349
423
350
* If a read-only attribute is not redeclared, it remains read-only::
424
351
352
+ @dataclass
425
353
class Game(HasTitle):
426
354
year: int
427
355
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
-
434
356
game = Game(title="Robot Wants Kitty", year=2010)
435
357
game.title = "Robot Wants Puppy" # error: "title" is read-only
358
+ game.year = 2012 # ok
436
359
437
360
* Subtypes can :external+typing:term: `narrow ` the type of read-only attributes::
438
361
@@ -526,10 +449,25 @@ Interaction with Other Type Qualifiers
526
449
This is consistent with the interaction of ``ReadOnly `` and :class: `typing.TypedDict `
527
450
defined in :pep: `705 `.
528
451
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.
533
471
534
472
535
473
Backwards Compatibility
@@ -570,8 +508,8 @@ following the footsteps of :pep:`705#how-to-teach-this`:
570
508
`type qualifiers <https://typing.python.org/en/latest/spec/qualifiers.html >`_ section:
571
509
572
510
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 >`_.
575
513
576
514
577
515
Rejected Ideas
@@ -588,26 +526,22 @@ quality of such properties.
588
526
This PEP makes ``ReadOnly `` a better alternative for defining read-only attributes
589
527
in protocols, superseding the use of properties for this purpose.
590
528
529
+ Assignment Only in ``__init__ `` and Class Scope
530
+ -----------------------------------------------
591
531
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 >`__.
599
535
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__ ``.
610
540
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.
611
545
612
546
Allowing Bare ``ReadOnly `` With Initializing Value
613
547
--------------------------------------------------
@@ -637,9 +571,6 @@ Footnotes
637
571
This PEP focuses solely on the type-checking behavior. Nevertheless, it should
638
572
be desirable the name is read-only at runtime.
639
573
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
-
643
574
644
575
Copyright
645
576
=========
0 commit comments