Skip to content

Commit a75e977

Browse files
committed
Subtyping
1 parent 7893b78 commit a75e977

File tree

1 file changed

+134
-13
lines changed

1 file changed

+134
-13
lines changed

peps/pep-0763.rst

Lines changed: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ there is no way of defining a :class:`~typing.Protocol`, such that the only
101101
requirements to satisfy are:
102102

103103
1. ``hasattr(obj, name)``
104-
2. ``isinstance(obj.name, T)`` [#invalid_typevar]_
104+
2. ``isinstance(obj.name, T)`` [#invalid_typevar]_
105105

106106
The above are satisfiable at runtime by all of the following:
107107

@@ -231,17 +231,18 @@ times the attribute can be assigned to in those contexts. Example:
231231
self.songs = list(songs)
232232
233233
def clear(self) -> None:
234-
# Type check error: assignment to read-only "songs" outside initialization
234+
# error: assignment to read-only "songs" outside initialization
235235
self.songs = []
236236
237237
238238
band = Band(name="Boa", songs=["Duvet"])
239-
band.name = "Emma" # Ok: "name" is not read-only
240-
band.songs = [] # Type check error: "songs" is read-only
241-
band.songs.append("Twilight") # Ok: list is mutable
239+
band.name = "Emma" # ok: "name" is not read-only
240+
band.songs = [] # error: "songs" is read-only
241+
band.songs.append("Twilight") # ok: list is mutable
242242
243243
Classes which do not define `__slots__ <https://docs.python.org/3/reference/datamodel.html#object.__slots__>`_
244-
may give the attribute a default value, overridable at instance level:
244+
retain the ability to initialize a read-only attribute both at declaration and
245+
within ``__init__``:
245246

246247
.. code-block:: python
247248
@@ -252,19 +253,138 @@ may give the attribute a default value, overridable at instance level:
252253
if number is not None:
253254
self.number = number
254255
255-
Inheritance
256-
-----------
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::
258+
259+
class Foo:
260+
id: ReadOnly[int] # error: "id" is not initialized on all code paths
261+
name: ReadOnly[str] # error: "name" is never initialized
262+
263+
def __init__(self) -> None:
264+
if random.random() > 0.5:
265+
self.id = 123
266+
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.
270+
271+
Subtyping
272+
---------
273+
274+
Read-only attributes are covariant. This has a few subtyping implications.
275+
Borrowing from :pep:`PEP 705 <705#inheritance>`:
276+
277+
* Read-only attributes can be redeclared as writable attributes, descriptors
278+
and class variables::
279+
280+
@dataclass
281+
class HasTitle:
282+
title: ReadOnly[str]
283+
284+
285+
@dataclass
286+
class Game(HasTitle):
287+
title: str
288+
year: int
289+
290+
291+
game = Game(title="DOOM", year=1993)
292+
game.year = 1994
293+
game.title = "DOOM II" # ok: attribute is not read-only
294+
295+
296+
class TitleProxy(HasTitle):
297+
@functools.cached_property
298+
def title(self) -> str: ...
299+
300+
301+
class SharedTitle(HasTitle):
302+
title: ClassVar[str] = "Still Grey"
303+
304+
* If a read-only attribute is not redeclared, it remains read-only::
305+
306+
@dataclass
307+
class Game(HasTitle):
308+
year: int
309+
310+
311+
game = Game(title="DOOM", year=1993)
312+
game.title = "DOOM II" # error: attribute is read-only
313+
314+
* Subtypes can narrow the type of read-only attributes::
315+
316+
class GameCollection(Protocol):
317+
games: ReadOnly[abc.Collection[Game]]
318+
319+
320+
@dataclass
321+
class GameSeries(GameCollection):
322+
name: str
323+
games: ReadOnly[list[Game]] # ok: list[Game] is assignable to Collection[Game]
324+
325+
* In nominal subclasses of protocols and ABCs, a read-only attribute should be
326+
considered abstract, unless it is initialized::
327+
328+
class MyBase(abc.ABC):
329+
foo: ReadOnly[int]
330+
bar: ReadOnly[str] = "abc"
331+
baz: ReadOnly[float]
332+
333+
def __init__(self, baz: float) -> None:
334+
self.baz = baz
335+
336+
@abstractmethod
337+
def do_something(self) -> None: ...
338+
339+
340+
@final
341+
class MySubclass(MyBase):
342+
# error: MySubclass does not override "foo"
343+
344+
def do_something(self) -> None:
345+
print(self.foo, self.bar, self.baz)
346+
347+
* In a protocol attribute declaration, ``name: ReadOnly[T]`` indicates that a structural
348+
subtype must support ``.name`` access, and the returned value is compatible with ``T``::
349+
350+
class HasName(Protocol):
351+
name: ReadOnly[str]
352+
353+
354+
class NamedAttr:
355+
name: str
356+
357+
class NamedProp:
358+
@property
359+
def name(self) -> str: ...
360+
361+
class NamedClassVar:
362+
name: ClassVar[str]
363+
364+
class NamedDescriptor:
365+
@cached_property
366+
def name(self) -> str: ...
367+
368+
# all of the following are ok
369+
has_name: HasName
370+
has_name = NamedAttr()
371+
has_name = NamedProp()
372+
has_name = NamedClassVar
373+
has_name = NamedClassVar()
374+
has_name = NamedDescriptor()
257375

258-
In a protocol attribute declaration, ``name: ReadOnly[T]`` indicates that a structural
259-
subtype must support ``.name`` access, and the returned value is compatible with ``T``.
260376

261377
Changes to ``Final``
262378
--------------------
263379

264380
.. TODO
265-
once changes are done, this probably won't be true
266-
``ReadOnly`` cannot be combined with ``Final``, as the two qualifiers differ in
267-
initialization rules, leading to ambiguity and/or significance of ordering.
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"?
268388
269389
Interaction with other special types
270390
------------------------------------
@@ -328,6 +448,7 @@ or attrs' `initialization hooks <https://www.attrs.org/en/stable/init.html#hooki
328448
This has been set aside for now, as defining rules regarding inclusion of
329449
such methods has proven difficult.
330450

451+
331452
Footnotes
332453
=========
333454

0 commit comments

Comments
 (0)