Skip to content

Commit 11f4522

Browse files
committed
Remove internal mutation restriction
1 parent afa3056 commit 11f4522

File tree

1 file changed

+99
-168
lines changed

1 file changed

+99
-168
lines changed

peps/pep-0767.rst

Lines changed: 99 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ Motivation
3030

3131
The Python type system lacks a single concise way to mark an attribute read-only.
3232
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.
3738

3839
.. _classes:
3940

@@ -167,6 +168,9 @@ A class with a read-only instance attribute can now be defined as::
167168
Specification
168169
=============
169170

171+
Usage
172+
-----
173+
170174
The :external+py3.13:data:`typing.ReadOnly` :external+typing:term:`type qualifier`
171175
becomes a valid annotation for :term:`attributes <attribute>` of classes and protocols.
172176
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
179183
self.name: ReadOnly[str] = name
180184

181185
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
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.
394321
This 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`
527450
defined 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

535473
Backwards 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

577515
Rejected Ideas
@@ -588,26 +526,22 @@ quality of such properties.
588526
This PEP makes ``ReadOnly`` a better alternative for defining read-only attributes
589527
in 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

612546
Allowing 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

Comments
 (0)