You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -6,7 +6,7 @@ Discussions-To: <REQUIRED: URL of current canonical discussion thread>
6
6
Status: Draft
7
7
Type: Standards Track
8
8
Topic: Typing
9
-
Created: 11-Oct-2024
9
+
Created: ?-Nov-2024
10
10
Python-Version: 3.?
11
11
12
12
@@ -18,7 +18,7 @@ to allow defining read-only :class:`typing.TypedDict` items.
18
18
19
19
This PEP proposes expanding the scope of ``ReadOnly`` to class and protocol
20
20
attributes, as a single concise way to mark them read-only. Some parity changes
21
-
are also made to :class:`typing.Final`.
21
+
are also made to :data:`typing.Final`.
22
22
23
23
Akin to :pep:`705`, it makes no Python grammar changes. Correct usage of
24
24
read-only attributes is intended to be enforced only by static type checkers.
@@ -27,7 +27,7 @@ read-only attributes is intended to be enforced only by static type checkers.
27
27
Motivation
28
28
==========
29
29
30
-
Python type system lacks a single concise way to mark an attribute read-only.
30
+
Python type system lacks a single concise way to mark an :term:`attribute` read-only.
31
31
This feature is common in other object-oriented languages (such as `C# <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly>`_),
32
32
and is useful for restricting attribute mutation at a type checker level, as well
33
33
as defining a broad interface for structural subtyping.
@@ -52,7 +52,7 @@ There are 3 major ways of achieving read-only attributes, honored by type checke
52
52
def __init__(self, number: int) -> None:
53
53
self.number: Final = number
54
54
55
-
- Supported by :mod:`dataclasses` (since `typing#1669 <https://github.com/python/typing/pull/1669>`_).
55
+
- Supported by :mod:`dataclasses` (and type checkers since `typing#1669 <https://github.com/python/typing/pull/1669>`_).
56
56
- Overriding ``number`` is not possible - the specification of ``Final``
57
57
imposes the name cannot be overridden in subclasses.
58
58
@@ -116,7 +116,7 @@ do not impact this.
116
116
117
117
The most common practice is to define such a protocol with a ``@property``::
118
118
119
-
class HasName[T](Protocol):
119
+
class HasName(Protocol):
120
120
@property
121
121
def name(self) -> T: ...
122
122
@@ -133,22 +133,20 @@ Worse yet, all of that is multiplied for each additional read-only attribute.
133
133
Rationale
134
134
=========
135
135
136
-
These problems can be resolved by an attribute-level type qualifier. ``ReadOnly``
137
-
has been chosen for this role, as its name conveys the intent well, and the newly
138
-
proposed changes complement its semantics defined in :pep:`705`.
136
+
These problems can be resolved by an attribute-level :external+typing:term:`type qualifier`.
137
+
``ReadOnly`` has been chosen for this role, as its name conveys the intent well,
138
+
and the newly proposed changes complement its semantics defined in the:pep:`705`.
139
139
140
140
A class with a read-only instance attribute can be now defined as such::
141
141
142
142
from typing import ReadOnly
143
143
144
144
145
145
class Member:
146
-
id: ReadOnly[int]
147
-
148
146
def __init__(self, id: int) -> None:
149
-
self.id = id
147
+
self.id: ReadOnly = id
150
148
151
-
...and a protocol as described in :ref:`protocols` is now just::
149
+
...and a protocol described in :ref:`protocols` is now just::
152
150
153
151
from typing import Protocol, ReadOnly
154
152
@@ -160,8 +158,8 @@ A class with a read-only instance attribute can be now defined as such::
160
158
def greet(obj: HasName, /) -> str:
161
159
return f"Hello, {obj.name}!"
162
160
163
-
* A subclass of ``Member`` can redefine ``id`` as a ``property`` or writable
164
-
attribute, while staying compatible with the base class.
161
+
* A subclass of ``Member`` can redefine ``.id`` as a writable attribute or a
162
+
:term:`descriptor`. It can also :external+typing:term:`narrow` the type.
165
163
* The ``HasName`` protocol can be implemented by any mechanism allowing for ``.name`` access.
166
164
* The ``greet`` function can now accept a wide variety of compatible objects,
167
165
while being explicit about no modifications being done to the input.
@@ -170,47 +168,53 @@ A class with a read-only instance attribute can be now defined as such::
170
168
Specification
171
169
=============
172
170
173
-
The :external+py3.13:data:`typing.ReadOnly` type qualifier becomes a valid annotation
174
-
for attributes of classes and protocols.
171
+
The :external+py3.13:data:`typing.ReadOnly` :external+typing:term:`type qualifier`
172
+
becomes a valid annotation for :term:`attributes <attribute>` of classes and protocols.
173
+
It is used to indicate that an attribute should not be reassigned or ``del``\ eted.
174
+
175
+
The deletability rule should be extended to ``Final`` as well, as it is currently
176
+
not specified.
175
177
176
-
It remains invalid in annotations of global and local variables, as in those contexts
177
-
it would have the same meaning as using ``Final``.
178
+
Akin to ``Final``, read-only attributes do not influence the mutability of
179
+
the assigned object. Immutable ABCs and containers may be used in combination with
180
+
``ReadOnly`` to prevent mutation of such values.
178
181
179
182
Syntax
180
183
------
181
184
182
-
``ReadOnly`` can be used at class-level or within ``__init__`` to declare
183
-
an attribute read-only:
185
+
``ReadOnly`` can be used at class-level or within ``__init__`` to mark individual
186
+
attributes read-only:
184
187
185
188
.. code-block:: python
186
189
187
-
classBase:
190
+
classBook:
188
191
id: ReadOnly[int]
189
192
190
-
def__init__(self, id: int, rate: float) -> None:
193
+
def__init__(self, id: int, name: str) -> None:
191
194
self.id =id
192
-
self.rate: ReadOnly =rate
195
+
self.name: ReadOnly =name
193
196
194
-
The explicit type in ``ReadOnly[<type>]`` can be omitted if an initializing value
195
-
is assigned to the attribute. A type checker should apply its usual type inference
196
-
rules to determine the type of ``rate``.
197
+
The explicit type in ``ReadOnly[<type>]`` can be omitted if the declaration has
198
+
an initializing value. A type checker should apply its usual type inference
199
+
rules to determine the type of ``name``.
197
200
198
-
In contexts where an attribute is already implied to be read-only, like in the
199
-
frozen :ref:`classes`, it should be valid to explicitly declare it ``ReadOnly``:
201
+
If an attribute is already implied to be read-only, like in the frozen :ref:`classes`,
202
+
explicit declarations should be permitted and seen as equivalent, except ``Final``
203
+
additionally forbids overriding in subclasses:
200
204
201
205
.. code-block:: python
202
206
203
207
@dataclass(frozen=True)
204
208
classPoint:
205
209
x: ReadOnly[int]
206
-
y: ReadOnly[int]
210
+
y: Final[int]
207
211
208
212
Initialization
209
213
--------------
210
214
211
215
Assignment to a ``ReadOnly`` attribute can only occur as a part of the declaration,
212
216
or within ``__init__`` of the same class. There is no restriction to how many
213
-
times the attribute can be assigned to in those contexts. Example:
217
+
times the attribute can be assigned to.
214
218
215
219
.. code-block:: python
216
220
@@ -235,38 +239,53 @@ times the attribute can be assigned to in those contexts. Example:
235
239
self.songs = []
236
240
237
241
238
-
band = Band(name="Boa", songs=["Duvet"])
239
-
band.name ="Emma"# ok: "name" is not read-only
242
+
band = Band(name="Bôa", songs=["Duvet"])
243
+
band.name ="Python"# ok: "name" is not read-only
240
244
band.songs = [] # error: "songs" is read-only
241
245
band.songs.append("Twilight") # ok: list is mutable
242
246
243
-
Classes which do not define `__slots__ <https://docs.python.org/3/reference/datamodel.html#object.__slots__>`_
244
-
retain the ability to initialize a read-only attribute both at declaration and
245
-
within ``__init__``:
247
+
248
+
classSubBand(Band):
249
+
def__init__(self) -> None:
250
+
# error: cannot assign to a read-only attribute of base class
251
+
self.songs = []
252
+
253
+
An initializing value at a class level can serve as a `flyweight <https://en.wikipedia.org/wiki/Flyweight_pattern>`_
254
+
default for instances:
246
255
247
256
.. code-block:: python
248
257
249
-
classFoo:
250
-
number: ReadOnly[int]=0
258
+
classPatient:
259
+
number: ReadOnly =0
251
260
252
261
def__init__(self, number: int|None=None) -> None:
253
262
if number isnotNone:
254
263
self.number = number
255
264
256
-
Type checkers should warn on attributes declared ``ReadOnly`` which may be left
257
-
uninitialized after ``__init__`` exits, unless the class is a protocol or an ABC::
265
+
This feature should also be supported by ``Final`` attributes. Specifically,
266
+
``Final`` attributes initialized in a class body **should no longer** imply ``ClassVar``,
267
+
and should remain assignable to within ``__init__``.
258
268
259
-
class Foo:
269
+
.. note::
270
+
Classes defining :data:`~object.__slots__` cannot make use of this feature.
271
+
An attribute with a class-level value cannot be included in slots,
272
+
effectively making it a class variable.
273
+
Type checkers may warn or suggest explicitly marking the attribute as a ``ClassVar``.
274
+
275
+
Type checkers should warn on read-only attributes which may be left uninitialized
276
+
after ``__init__`` exits, except in :external+typing:term:`stubs <stub>`, protocols or ABCs::
277
+
278
+
class Patient:
260
279
id: ReadOnly[int] # error: "id" is not initialized on all code paths
261
280
name: ReadOnly[str] # error: "name" is never initialized
262
281
263
282
def __init__(self) -> None:
264
283
if random.random() > 0.5:
265
284
self.id = 123
266
285
267
-
This rule stems from the fact the body of the class declaring the attribute is
268
-
the only place able to initialize it, in contrast to non-read-only attributes,
269
-
which could be initialized outside of the class body.
286
+
287
+
class HasName(Protocol):
288
+
name: ReadOnly[str] # ok
270
289
271
290
Subtyping
272
291
---------
@@ -275,7 +294,7 @@ Read-only attributes are covariant. This has a few subtyping implications.
275
294
Borrowing from :pep:`PEP 705 <705#inheritance>`:
276
295
277
296
* Read-only attributes can be redeclared as writable attributes, descriptors
278
-
and class variables::
297
+
or class variables::
279
298
280
299
@dataclass
281
300
class HasTitle:
@@ -311,7 +330,7 @@ Borrowing from :pep:`PEP 705 <705#inheritance>`:
311
330
game = Game(title="DOOM", year=1993)
312
331
game.title = "DOOM II" # error: attribute is read-only
313
332
314
-
* Subtypes can narrow the type of read-only attributes::
333
+
* Subtypes can :external+typing:term:`narrow` the type of read-only attributes::
315
334
316
335
class GameCollection(Protocol):
317
336
games: ReadOnly[abc.Collection[Game]]
@@ -322,8 +341,8 @@ Borrowing from :pep:`PEP 705 <705#inheritance>`:
322
341
name: str
323
342
games: ReadOnly[list[Game]] # ok: list[Game] is assignable to Collection[Game]
324
343
325
-
* In nominal subclasses of protocols and ABCs, a read-only attribute should be
326
-
considered abstract, unless it is initialized::
344
+
* Nominal subclasses of protocols and ABCs should redeclare read-only attributes
345
+
in order to implement them, unless the base class initializes them in some way::
327
346
328
347
class MyBase(abc.ABC):
329
348
foo: ReadOnly[int]
@@ -334,14 +353,14 @@ Borrowing from :pep:`PEP 705 <705#inheritance>`:
334
353
self.baz = baz
335
354
336
355
@abstractmethod
337
-
def do_something(self) -> None: ...
356
+
def pprint(self) -> None: ...
338
357
339
358
340
359
@final
341
360
class MySubclass(MyBase):
342
361
# error: MySubclass does not override "foo"
343
362
344
-
def do_something(self) -> None:
363
+
def pprint(self) -> None:
345
364
print(self.foo, self.bar, self.baz)
346
365
347
366
* In a protocol attribute declaration, ``name: ReadOnly[T]`` indicates that a structural
@@ -373,19 +392,6 @@ Borrowing from :pep:`PEP 705 <705#inheritance>`:
373
392
has_name = NamedClassVar()
374
393
has_name = NamedDescriptor()
375
394
376
-
377
-
Changes to ``Final``
378
-
--------------------
379
-
380
-
.. TODO
381
-
- deletability
382
-
- final in protocols?
383
-
- interaction of Final and ReadOnly - once parity changes are done, the
384
-
following shouldn't be true:
385
-
``ReadOnly`` cannot be combined with ``Final``, as the two qualifiers differ in
386
-
initialization rules, leading to ambiguity and/or significance of ordering.
387
-
- section on "Type consistency"?
388
-
389
395
Interaction with other special types
390
396
------------------------------------
391
397
@@ -409,6 +415,9 @@ defined in :pep:`705`.
409
415
``ClassVar`` excludes read-only attributes from being assignable to within
410
416
initialization methods.
411
417
418
+
Rules of ``Final`` should take priority when combined with ``ReadOnly``. As such,
419
+
type checkers may warn on the redundancy of combining the two type qualifiers.
420
+
412
421
413
422
Backwards Compatibility
414
423
=======================
@@ -435,18 +444,81 @@ How to Teach This
435
444
[How to teach users, new and experienced, how to apply the PEP to their work.]
436
445
437
446
438
-
Rejected ideas
439
-
==============
447
+
Open Issues
448
+
===========
449
+
450
+
Assignment in ``__new__``
451
+
-------------------------
452
+
453
+
Immutable classes like :class:`fractions.Fraction` often do not define ``__init__``;
454
+
instead, they perform initialization in ``__new__`` or classmethods. The proposed
455
+
feature won't be useful to them.
456
+
457
+
OTOH, allowing assignment within ``__new__`` (and/or classmethods) could open way
# assignment to an object which has been initialized before,
478
+
# breaking the invariant a read-only attribute can be assigned to
479
+
# only during its initialization?
480
+
self.foo = foo
481
+
482
+
cls.object_cache[foo] =self
483
+
returnself
484
+
485
+
To my understanding, properly detecting this problem would require type checkers
486
+
to keep track of the "level of initialization" of an object.
487
+
488
+
This issue doesn't seem to impact ``__init__``, since it's rather uncommon to
489
+
ever rebind ``self`` within it to any other object, and type checkers could
490
+
flag the action as whole.
491
+
492
+
493
+
Extending initialization
494
+
------------------------
495
+
496
+
Mechanisms such as :func:`dataclasses.__post_init__` or attrs' `initialization hooks <https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization>`_
497
+
augment initialization by providing a set of dunder hooks which will be called
498
+
once during instance creation. The current rules would disallow assignment in those
499
+
hooks. Specifying any single method in the pep isn't enough, as the naming and
500
+
functionality differs between mechanisms (``__post_init__`` vs ``__attrs_post_init__``).
501
+
502
+
``ReadOnly[ClassVar[...]]`` and ``__init_subclass__``
0 commit comments