PEP 767: Annotating Read-Only Attributes (#4127)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> Co-authored-by: Carl Meyer <carl@oddbird.net> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
This commit is contained in:
parent
8b84ca20ba
commit
ed44bb0655
|
@ -643,6 +643,7 @@ peps/pep-0762.rst @pablogsal @ambv @lysnikolaou @emilyemorehouse
|
|||
peps/pep-0763.rst @dstufft
|
||||
peps/pep-0765.rst @iritkatriel @ncoghlan
|
||||
peps/pep-0766.rst @warsaw
|
||||
peps/pep-0767.rst @carljm
|
||||
# ...
|
||||
peps/pep-0777.rst @warsaw
|
||||
# ...
|
||||
|
|
|
@ -0,0 +1,650 @@
|
|||
PEP: 767
|
||||
Title: Annotating Read-Only Attributes
|
||||
Author: Eneg <eneg at discuss.python.org>
|
||||
Sponsor: Carl Meyer <carl@oddbird.net>
|
||||
Discussions-To: https://discuss.python.org/t/expanding-readonly-to-normal-classes-protocols/67359
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Topic: Typing
|
||||
Created: 18-Nov-2024
|
||||
Python-Version: 3.14
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
:pep:`705` introduced the :external+py3.13:data:`typing.ReadOnly` type qualifier
|
||||
to allow defining read-only :class:`typing.TypedDict` items.
|
||||
|
||||
This PEP proposes using ``ReadOnly`` in :term:`annotations <annotation>` of class and protocol
|
||||
:term:`attributes <attribute>`, as a single concise way to mark them read-only.
|
||||
|
||||
Akin to :pep:`705`, it makes no changes to setting attributes at runtime. Correct
|
||||
usage of read-only attributes is intended to be enforced only by static type checkers.
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
The Python type system lacks a single concise way to mark an attribute read-only.
|
||||
This feature is present in other statically and gradually typed languages
|
||||
(such as `C# <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly>`_
|
||||
or `TypeScript <https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties>`_),
|
||||
and is useful for removing the ability to reassign or ``del``\ ete an attribute
|
||||
at a type checker level, as well as defining a broad interface for structural subtyping.
|
||||
|
||||
.. _classes:
|
||||
|
||||
Classes
|
||||
-------
|
||||
|
||||
Today, there are three major ways of achieving read-only attributes, honored by type checkers:
|
||||
|
||||
* annotating the attribute with :data:`typing.Final`::
|
||||
|
||||
class Foo:
|
||||
number: Final[int]
|
||||
|
||||
def __init__(self, number: int) -> None:
|
||||
self.number = number
|
||||
|
||||
|
||||
class Bar:
|
||||
def __init__(self, number: int) -> None:
|
||||
self.number: Final = number
|
||||
|
||||
- Supported by :mod:`dataclasses` (and type checkers since `typing#1669 <https://github.com/python/typing/pull/1669>`_).
|
||||
- Overriding ``number`` is not possible - the specification of ``Final``
|
||||
imposes that the name cannot be overridden in subclasses.
|
||||
|
||||
* read-only proxy via ``@property``::
|
||||
|
||||
class Foo:
|
||||
_number: int
|
||||
|
||||
def __init__(self, number: int) -> None:
|
||||
self._number = number
|
||||
|
||||
@property
|
||||
def number(self) -> int:
|
||||
return self._number
|
||||
|
||||
- Overriding ``number`` is possible. *Type checkers disagree about the specific rules*. [#overriding_property]_
|
||||
- Read-only at runtime. [#runtime]_
|
||||
- Requires extra boilerplate.
|
||||
- Supported by :mod:`dataclasses`, but does not compose well - the synthesized
|
||||
``__init__`` and ``__repr__`` will use ``_number`` as the parameter/attribute name.
|
||||
|
||||
* using a "freezing" mechanism, such as :func:`dataclasses.dataclass` or :class:`typing.NamedTuple`::
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Foo:
|
||||
number: int # implicitly read-only
|
||||
|
||||
|
||||
class Bar(NamedTuple):
|
||||
number: int # implicitly read-only
|
||||
|
||||
- Overriding ``number`` is possible in the ``@dataclass`` case.
|
||||
- Read-only at runtime. [#runtime]_
|
||||
- No per-attribute control - these mechanisms apply to the whole class.
|
||||
- Frozen dataclasses incur some runtime overhead.
|
||||
- ``NamedTuple`` is still a ``tuple``. Most classes do not need to inherit
|
||||
indexing, iteration, or concatenation.
|
||||
|
||||
.. _protocols:
|
||||
|
||||
Protocols
|
||||
---------
|
||||
|
||||
A read-only attribute ``name: T`` on a :class:`~typing.Protocol` in principle
|
||||
defines two requirements:
|
||||
|
||||
1. ``hasattr(obj, "name")``
|
||||
2. ``isinstance(obj.name, T)``
|
||||
|
||||
Those requirements are satisfiable at runtime by all of the following:
|
||||
|
||||
* an object with an attribute ``name: T``,
|
||||
* a class with a class variable ``name: ClassVar[T]``,
|
||||
* an instance of the class above,
|
||||
* an object with a ``@property`` ``def name(self) -> T``,
|
||||
* an object with a custom descriptor, such as :func:`functools.cached_property`.
|
||||
|
||||
The current `typing spec <https://typing.readthedocs.io/en/latest/spec/protocol.html#protocol-members>`_
|
||||
allows creation of such protocol members using (abstract) properties::
|
||||
|
||||
class HasName(Protocol):
|
||||
@property
|
||||
def name(self) -> T: ...
|
||||
|
||||
This syntax has several drawbacks:
|
||||
|
||||
* It is somewhat verbose.
|
||||
* It is not obvious that the quality conveyed here is the read-only character of a property.
|
||||
* It is not composable with :external+typing:term:`type qualifiers <type qualifier>`.
|
||||
* Not all type checkers agree [#property_in_protocol]_ that all of the above five
|
||||
objects are assignable to this structural type.
|
||||
|
||||
Rationale
|
||||
=========
|
||||
|
||||
These problems can be resolved by an attribute-level type qualifier.
|
||||
``ReadOnly`` has been chosen for this role, as its name conveys the intent well,
|
||||
and the newly proposed changes complement its semantics defined in :pep:`705`.
|
||||
|
||||
A class with a read-only instance attribute can now be defined as::
|
||||
|
||||
from typing import ReadOnly
|
||||
|
||||
|
||||
class Member:
|
||||
def __init__(self, id: int) -> None:
|
||||
self.id: ReadOnly[int] = id
|
||||
|
||||
...and the protocol described in :ref:`protocols` is now just::
|
||||
|
||||
from typing import Protocol, ReadOnly
|
||||
|
||||
|
||||
class HasName(Protocol):
|
||||
name: ReadOnly[str]
|
||||
|
||||
|
||||
def greet(obj: HasName, /) -> str:
|
||||
return f"Hello, {obj.name}!"
|
||||
|
||||
* A subclass of ``Member`` can redefine ``.id`` as a writable attribute or a
|
||||
:term:`descriptor`. It can also :external+typing:term:`narrow` the type.
|
||||
* The ``HasName`` protocol has a more succinct definition, and is agnostic
|
||||
to the writability of the attribute.
|
||||
* The ``greet`` function can now accept a wide variety of compatible objects,
|
||||
while being explicit about no modifications being done to the input.
|
||||
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
The :external+py3.13:data:`typing.ReadOnly` :external+typing:term:`type qualifier`
|
||||
becomes a valid annotation for :term:`attributes <attribute>` of classes and protocols.
|
||||
It can be used at class-level or within ``__init__`` to mark individual attributes read-only::
|
||||
|
||||
class Book:
|
||||
id: ReadOnly[int]
|
||||
|
||||
def __init__(self, id: int, name: str) -> None:
|
||||
self.id = id
|
||||
self.name: ReadOnly[str] = name
|
||||
|
||||
Type checkers should error on any attempt to reassign or ``del``\ ete an attribute
|
||||
annotated with ``ReadOnly``.
|
||||
Type checkers should also error on any attempt to delete an attribute annotated as ``Final``.
|
||||
(This is not currently specified.)
|
||||
|
||||
Use of ``ReadOnly`` in annotations at other sites where it currently has no meaning
|
||||
(such as local/global variables or function parameters) is considered out of scope
|
||||
for this PEP.
|
||||
|
||||
Akin to ``Final`` [#final_mutability]_, ``ReadOnly`` does not influence how
|
||||
type checkers perceive the mutability of the assigned object. Immutable :term:`ABCs <abstract base class>`
|
||||
and :mod:`containers <collections.abc>` may be used in combination with ``ReadOnly``
|
||||
to forbid mutation of such values at a type checker level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from collections import abc
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol, ReadOnly
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game:
|
||||
name: str
|
||||
|
||||
|
||||
class HasGames[T: abc.Collection[Game]](Protocol):
|
||||
games: ReadOnly[T]
|
||||
|
||||
|
||||
def add_games(shelf: HasGames[list[Game]]) -> None:
|
||||
shelf.games.append(Game("Half-Life")) # ok: list is mutable
|
||||
shelf.games[-1].name = "Black Mesa" # ok: "name" is not read-only
|
||||
shelf.games = [] # error: "games" is read-only
|
||||
del shelf.games # error: "games" is read-only and cannot be deleted
|
||||
|
||||
|
||||
def read_games(shelf: HasGames[abc.Sequence[Game]]) -> None:
|
||||
shelf.games.append(...) # error: "Sequence" has no attribute "append"
|
||||
shelf.games[0].name = "Blue Shift" # ok: "name" is not read-only
|
||||
shelf.games = [] # error: "games" is read-only
|
||||
|
||||
|
||||
All instance attributes of frozen dataclasses and ``NamedTuple`` should be
|
||||
implied to be read-only. Type checkers may inform that annotating such attributes
|
||||
with ``ReadOnly`` is redundant, but it should not be seen as an error:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import NewType, ReadOnly
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Point:
|
||||
x: int # implicit read-only
|
||||
y: ReadOnly[int] # ok, redundant
|
||||
|
||||
|
||||
uint = NewType("uint", int)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnsignedPoint(Point):
|
||||
x: ReadOnly[uint] # ok, redundant; narrower type
|
||||
y: Final[uint] # not redundant, Final imposes extra restrictions; narrower type
|
||||
|
||||
.. _init:
|
||||
|
||||
Initialization
|
||||
--------------
|
||||
|
||||
Assignment to a read-only attribute can only occur in the class declaring the attribute.
|
||||
There is no restriction to how many times the attribute can be assigned to.
|
||||
The assignment must be allowed in the following contexts:
|
||||
|
||||
* In ``__init__``, on the instance received as the first parameter (likely, ``self``).
|
||||
* In ``__new__``, on instances of the declaring class created via a call
|
||||
to a super-class' ``__new__`` method.
|
||||
* At declaration in the body of the class.
|
||||
|
||||
Additionally, a type checker may choose to allow the assignment:
|
||||
|
||||
* In ``__new__``, on instances of the declaring class, without regard
|
||||
to the origin of the instance.
|
||||
(This choice trades soundness, as the instance may already be initialized,
|
||||
for the simplicity of implementation.)
|
||||
* In ``@classmethod``\ s, on instances of the declaring class created via
|
||||
a call to the class' or super-class' ``__new__`` method.
|
||||
|
||||
Note that a child class cannot assign to any read-only attributes of a parent class
|
||||
in any of the aforementioned contexts, unless the attribute is redeclared.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from collections import abc
|
||||
from typing import ReadOnly
|
||||
|
||||
|
||||
class Band:
|
||||
name: str
|
||||
songs: ReadOnly[list[str]]
|
||||
|
||||
def __init__(self, name: str, songs: abc.Iterable[str] | None = None) -> None:
|
||||
self.name = name
|
||||
self.songs = []
|
||||
|
||||
if songs is not None:
|
||||
self.songs = list(songs) # multiple assignments are fine
|
||||
|
||||
def clear(self) -> None:
|
||||
# error: assignment to read-only "songs" outside initialization
|
||||
self.songs = []
|
||||
|
||||
|
||||
band = Band(name="Bôa", songs=["Duvet"])
|
||||
band.name = "Python" # ok: "name" is not read-only
|
||||
band.songs = [] # error: "songs" is read-only
|
||||
band.songs.append("Twilight") # ok: list is mutable
|
||||
|
||||
|
||||
class SubBand(Band):
|
||||
def __init__(self) -> None:
|
||||
self.songs = [] # error: cannot assign to a read-only attribute of a base class
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# a simplified immutable Fraction class
|
||||
class Fraction:
|
||||
numerator: ReadOnly[int]
|
||||
denominator: ReadOnly[int]
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
numerator: str | int | float | Decimal | Rational = 0,
|
||||
denominator: int | Rational | None = None
|
||||
) -> Self:
|
||||
self = super().__new__(cls)
|
||||
|
||||
if denominator is None:
|
||||
if type(numerator) is int:
|
||||
self.numerator = numerator
|
||||
self.denominator = 1
|
||||
return self
|
||||
|
||||
elif isinstance(numerator, Rational): ...
|
||||
|
||||
else: ...
|
||||
|
||||
@classmethod
|
||||
def from_float(cls, f: float, /) -> Self:
|
||||
self = super().__new__(cls)
|
||||
self.numerator, self.denominator = f.as_integer_ratio()
|
||||
return self
|
||||
|
||||
When a class-level declaration has an initializing value, it can serve as a `flyweight <https://en.wikipedia.org/wiki/Flyweight_pattern>`_
|
||||
default for instances:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Patient:
|
||||
number: ReadOnly[int] = 0
|
||||
|
||||
def __init__(self, number: int | None = None) -> None:
|
||||
if number is not None:
|
||||
self.number = number
|
||||
|
||||
.. note::
|
||||
This feature conflicts with :data:`~object.__slots__`. An attribute with
|
||||
a class-level value cannot be included in slots, effectively making it a class variable.
|
||||
|
||||
Type checkers may choose to warn on read-only attributes which could be left uninitialized
|
||||
after an instance is created (except in :external+typing:term:`stubs <stub>`,
|
||||
protocols or ABCs)::
|
||||
|
||||
class Patient:
|
||||
id: ReadOnly[int] # error: "id" is not initialized on all code paths
|
||||
name: ReadOnly[str] # error: "name" is never initialized
|
||||
|
||||
def __init__(self) -> None:
|
||||
if random.random() > 0.5:
|
||||
self.id = 123
|
||||
|
||||
|
||||
class HasName(Protocol):
|
||||
name: ReadOnly[str] # ok
|
||||
|
||||
Subtyping
|
||||
---------
|
||||
|
||||
Read-only attributes are covariant. This has a few subtyping implications.
|
||||
Borrowing from :pep:`705#inheritance`:
|
||||
|
||||
* Read-only attributes can be redeclared as writable attributes, descriptors
|
||||
or class variables::
|
||||
|
||||
@dataclass
|
||||
class HasTitle:
|
||||
title: ReadOnly[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game(HasTitle):
|
||||
title: str
|
||||
year: int
|
||||
|
||||
|
||||
game = Game(title="DOOM", year=1993)
|
||||
game.year = 1994
|
||||
game.title = "DOOM II" # ok: attribute is not read-only
|
||||
|
||||
|
||||
class TitleProxy(HasTitle):
|
||||
@functools.cached_property
|
||||
def title(self) -> str: ...
|
||||
|
||||
|
||||
class SharedTitle(HasTitle):
|
||||
title: ClassVar[str] = "Still Grey"
|
||||
|
||||
* If a read-only attribute is not redeclared, it remains read-only::
|
||||
|
||||
class Game(HasTitle):
|
||||
year: int
|
||||
|
||||
def __init__(self, title: str, year: int) -> None:
|
||||
super().__init__(title)
|
||||
self.title = title # error: cannot assign to a read-only attribute of base class
|
||||
self.year = year
|
||||
|
||||
|
||||
game = Game(title="Robot Wants Kitty", year=2010)
|
||||
game.title = "Robot Wants Puppy" # error: "title" is read-only
|
||||
|
||||
* Subtypes can :external+typing:term:`narrow` the type of read-only attributes::
|
||||
|
||||
class GameCollection(Protocol):
|
||||
games: ReadOnly[abc.Collection[Game]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameSeries(GameCollection):
|
||||
name: str
|
||||
games: ReadOnly[list[Game]] # ok: list[Game] is assignable to Collection[Game]
|
||||
|
||||
* Nominal subclasses of protocols and ABCs should redeclare read-only attributes
|
||||
in order to implement them, unless the base class initializes them in some way::
|
||||
|
||||
class MyBase(abc.ABC):
|
||||
foo: ReadOnly[int]
|
||||
bar: ReadOnly[str] = "abc"
|
||||
baz: ReadOnly[float]
|
||||
|
||||
def __init__(self, baz: float) -> None:
|
||||
self.baz = baz
|
||||
|
||||
@abstractmethod
|
||||
def pprint(self) -> None: ...
|
||||
|
||||
|
||||
@final
|
||||
class MySubclass(MyBase):
|
||||
# error: MySubclass does not override "foo"
|
||||
|
||||
def pprint(self) -> None:
|
||||
print(self.foo, self.bar, self.baz)
|
||||
|
||||
* In a protocol attribute declaration, ``name: ReadOnly[T]`` indicates that a structural
|
||||
subtype must support ``.name`` access, and the returned value is assignable to ``T``::
|
||||
|
||||
class HasName(Protocol):
|
||||
name: ReadOnly[str]
|
||||
|
||||
|
||||
class NamedAttr:
|
||||
name: str
|
||||
|
||||
class NamedProp:
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
|
||||
class NamedClassVar:
|
||||
name: ClassVar[str]
|
||||
|
||||
class NamedDescriptor:
|
||||
@cached_property
|
||||
def name(self) -> str: ...
|
||||
|
||||
# all of the following are ok
|
||||
has_name: HasName
|
||||
has_name = NamedAttr()
|
||||
has_name = NamedProp()
|
||||
has_name = NamedClassVar
|
||||
has_name = NamedClassVar()
|
||||
has_name = NamedDescriptor()
|
||||
|
||||
Interaction with Other Type Qualifiers
|
||||
--------------------------------------
|
||||
|
||||
``ReadOnly`` can be used with ``ClassVar`` and ``Annotated`` in any nesting order:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Foo:
|
||||
foo: ClassVar[ReadOnly[str]] = "foo"
|
||||
bar: Annotated[ReadOnly[int], Gt(0)]
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Foo:
|
||||
foo: ReadOnly[ClassVar[str]] = "foo"
|
||||
bar: ReadOnly[Annotated[int, Gt(0)]]
|
||||
|
||||
This is consistent with the interaction of ``ReadOnly`` and :class:`typing.TypedDict`
|
||||
defined in :pep:`705`.
|
||||
|
||||
An attribute annotated as both ``ReadOnly`` and ``ClassVar`` can only be assigned to
|
||||
at declaration in the class body.
|
||||
|
||||
An attribute cannot be annotated as both ``ReadOnly`` and ``Final``, as the two
|
||||
qualifiers differ in semantics, and ``Final`` is generally more restrictive.
|
||||
``Final`` remains allowed as an annotation of attributes that are only implied
|
||||
to be read-only. It can be also used to redeclare a ``ReadOnly`` attribute of a base class.
|
||||
|
||||
|
||||
Backwards Compatibility
|
||||
=======================
|
||||
|
||||
This PEP introduces new contexts where ``ReadOnly`` is valid. Programs inspecting
|
||||
those places will have to change to support it. This is expected to mainly affect type checkers.
|
||||
|
||||
However, caution is advised while using the backported ``typing_extensions.ReadOnly``
|
||||
in older versions of Python. Mechanisms inspecting annotations may behave incorrectly
|
||||
when encountering ``ReadOnly``; in particular, the ``@dataclass`` decorator
|
||||
which `looks for <https://docs.python.org/3/library/dataclasses.html#class-variables>`_
|
||||
``ClassVar`` may mistakenly treat ``ReadOnly[ClassVar[...]]`` as an instance attribute.
|
||||
|
||||
To avoid issues with introspection, use ``ClassVar[ReadOnly[...]]`` instead of ``ReadOnly[ClassVar[...]]``.
|
||||
|
||||
|
||||
Security Implications
|
||||
=====================
|
||||
|
||||
There are no known security consequences arising from this PEP.
|
||||
|
||||
|
||||
How to Teach This
|
||||
=================
|
||||
|
||||
Suggested changes to the :mod:`typing` module documentation,
|
||||
following the footsteps of :pep:`705#how-to-teach-this`:
|
||||
|
||||
* Add this PEP to the others listed.
|
||||
* Link :external+py3.13:data:`typing.ReadOnly` to this PEP.
|
||||
* Update the description of ``typing.ReadOnly``:
|
||||
|
||||
A special typing construct to mark an attribute of a class or an item of
|
||||
a ``TypedDict`` as read-only.
|
||||
|
||||
* Add a standalone entry for ``ReadOnly`` under the
|
||||
`type qualifiers <https://typing.readthedocs.io/en/latest/spec/qualifiers.html>`_ section:
|
||||
|
||||
The ``ReadOnly`` type qualifier in class attribute annotations indicates
|
||||
that the attribute of the class may be read, but not reassigned or ``del``\ eted.
|
||||
For usage in ``TypedDict``, see `ReadOnly <https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier>`_.
|
||||
|
||||
|
||||
Rejected Ideas
|
||||
==============
|
||||
|
||||
Clarifying Interaction of ``@property`` and Protocols
|
||||
-----------------------------------------------------
|
||||
|
||||
The :ref:`protocols` section mentions an inconsistency between type checkers in
|
||||
the interpretation of properties in protocols. The problem could be fixed
|
||||
by amending the typing specification, clarifying what implements the read-only
|
||||
quality of such properties.
|
||||
|
||||
This PEP makes ``ReadOnly`` a better alternative for defining read-only attributes
|
||||
in protocols, superseding the use of properties for this purpose.
|
||||
|
||||
|
||||
Assignment Only in ``__init__`` and Class Body
|
||||
----------------------------------------------
|
||||
|
||||
An earlier version of this PEP proposed that read-only attributes could only be
|
||||
assigned to in ``__init__`` and the class' body. A later discussion revealed that
|
||||
this restriction would severely limit the usability of ``ReadOnly`` within
|
||||
immutable classes, which typically do not define ``__init__``.
|
||||
|
||||
:class:`fractions.Fraction` is one example of an immutable class, where the
|
||||
initialization of its attributes happens within ``__new__`` and classmethods.
|
||||
However, unlike in ``__init__``, the assignment in ``__new__`` and classmethods
|
||||
is potentially unsound, as the instance they work on can be sourced from
|
||||
an arbitrary place, including an already finalized instance.
|
||||
|
||||
We find it imperative that this type checking feature is useful to the foremost
|
||||
use site of read-only attributes - immutable classes. Thus, the PEP has changed
|
||||
since to allow assignment in ``__new__`` and classmethods under a set of rules
|
||||
described in the :ref:`init` section.
|
||||
|
||||
|
||||
Open Issues
|
||||
===========
|
||||
|
||||
Extending Initialization
|
||||
------------------------
|
||||
|
||||
Mechanisms such as :func:`dataclasses.__post_init__` or attrs' `initialization hooks <https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization>`_
|
||||
augment object creation by providing a set of special hooks which are called
|
||||
during initialization.
|
||||
|
||||
The current initialization rules defined in this PEP disallow assignment to
|
||||
read-only attributes in such methods. It is unclear whether the rules could be
|
||||
satisfyingly shaped in a way that is inclusive of those 3rd party hooks, while
|
||||
upkeeping the invariants associated with the read-only-ness of those attributes.
|
||||
|
||||
The Python type system has a long and detailed `specification <https://typing.readthedocs.io/en/latest/spec/constructors.html>`_
|
||||
regarding the behavior of ``__new__`` and ``__init__``. It is rather unfeasible
|
||||
to expect the same level of detail from 3rd party hooks.
|
||||
|
||||
A potential solution would involve type checkers providing configuration in this
|
||||
regard, requiring end users to manually specify a set of methods they wish
|
||||
to allow initialization in. This however could easily result in users mistakenly
|
||||
or purposefully breaking the aforementioned invariants. It is also a fairly
|
||||
big ask for a relatively niche feature.
|
||||
|
||||
``ReadOnly[ClassVar[...]]`` and ``__init_subclass__``
|
||||
-----------------------------------------------------
|
||||
|
||||
Should read-only class variables be assignable to within the declaring class'
|
||||
``__init_subclass__``?
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class URI:
|
||||
protocol: ReadOnly[ClassVar[str]] = ""
|
||||
|
||||
def __init_subclass__(cls, protocol: str = "") -> None:
|
||||
cls.foo = protocol
|
||||
|
||||
class File(URI, protocol="file"): ...
|
||||
|
||||
|
||||
Footnotes
|
||||
=========
|
||||
|
||||
.. [#overriding_property]
|
||||
Pyright in strict mode disallows non-property overrides.
|
||||
Mypy does not impose this restriction and allows an override with a plain attribute.
|
||||
`[Pyright playground] <https://pyright-play.net/?strict=true&code=MYGwhgzhAEBiD28BcBYAUNT0D6A7ArgLYBGApgE5LQCWuALuultACakBmO2t1d22ACgikQ7ADTQCJClVp0AlNAC0APmgA5eLlKoMzLMNEA6PETLloAXklmKjPZgACAB3LxnFOgE8mWNpylzIRF2RVUael19LHJSOnxyXGhDdhNAuzR7UEgYACEwcgEEeHkorHTKCIY0IA>`_
|
||||
`[mypy playground] <https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=6f860a865c5d13cce07d6cbb08b9fb85>`_
|
||||
|
||||
.. [#runtime]
|
||||
This PEP focuses solely on the type-checking behavior. Nevertheless, it should
|
||||
be desirable the name is read-only at runtime.
|
||||
|
||||
.. [#property_in_protocol]
|
||||
Pyright disallows class variable and non-property descriptor overrides.
|
||||
`[Pyright] <https://pyright-play.net/?pyrightVersion=1.1.389&pythonVersion=3.13&strict=true&code=GYJw9gtgBAhgRgYygSwgBzCALrOBnLEGBLCAUywAswATAKFEimAFcA7EsMAGzxXUw4ExSmRoB9NODRlsATwbhoWOWmRsA5vwzYoAYW4w8eAGowQAGigAFcFjAIeV4Opjc6HhIeNQAEkYAxLgAKWzB7R24ASgAuOigEqAABKTAZeXjEpPgCIhJyKlpMhJoyYGYQvDJuYCioAFoAPhQ2LBioADoujzoAYlhjZA02eGRuZBUeryM%2BILAAQSxCZDgWLDI4xIqwdvUsT29ZrjD0lU2s1NOFLdLy4Erq2obmvfaQChYQNigABgOZqBzAwzMwgc4Je47fSHUEAbT2AF0oABeX7-HxzAAiZDwCBAyDQ9jBxWSwgQogkl1kkxuZW2wSqNTqTRabSg7ywn2%2Bfzo0wxx2k1LkejAADdzMgYK1wckqRlaXcHkznlA4FxuG8Pl9AW4quijmAAJJscXjGgyyHtXL6qAAOTAcxlcHMVsIPTAcAAVu1-Hg5nQPZ6UYCuItlqt1sE6lB%2BmAANYBr3BuYnIVRxKxhOB5NcYHGUFbGNQeOJoOooEw8zphKZ0s5sDY3H4wmYdO17PlgVpIUi8X4qVYNvFrNJztGk1uZA0as1qCyEB11H2uYzwtF%2BeLu1gNhkNdr-objwHgAeaHGCAm2ncNrmYfxEbIhvQ3GCvrmsRJltZN67VyfZ9fQIuA-LYUkFeVEluelGSeFlXnZLVuR-MA81Mcx-xfN9gItLh2lQuFEWDHk%2BQNRs8QJIkMMAv1sJJJIyQpSRwJpSC6UhBlHmZF5pQQzltWIw4QzAVN5F7CUByorCwBAi5mOuVjFTADjlRZNUeE1PjvgCXUyGQ41TSnSSgOknCoWtOgkhcEZ3BIrc5iMmiTJJZ0wSga0gA>`_
|
||||
`[mypy] <https://mypy-play.net/?mypy=1.13.0&python=3.12&flags=strict&gist=12d556bb6ef4a9a49ff4ed4776604750>`_
|
||||
`[Pyre] <https://pyre-check.org/play/?input=%23%20pyre-strict%0Afrom%20abc%20import%20abstractmethod%0Afrom%20functools%20import%20cached_property%0Afrom%20typing%20import%20ClassVar%2C%20Protocol%2C%20final%0A%0A%0Aclass%20HasFoo(Protocol)%3A%0A%20%20%20%20%40property%0A%20%20%20%20%40abstractmethod%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20...%0A%0A%0A%23%20assignability%0A%0A%0Aclass%20FooAttribute%3A%0A%20%20%20%20foo%3A%20int%0A%0Aclass%20FooProperty%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooClassVar%3A%0A%20%20%20%20foo%3A%20ClassVar%5Bint%5D%20%3D%200%0A%0Aclass%20FooDescriptor%3A%0A%20%20%20%20%40cached_property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooPropertyCovariant%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20bool%3A%20return%20False%0A%0Aclass%20FooInvalid%3A%0A%20%20%20%20foo%3A%20str%0A%0Aclass%20NoFoo%3A%0A%20%20%20%20bar%3A%20str%0A%0A%0Aobj%3A%20HasFoo%0Aobj%20%3D%20FooAttribute()%20%20%23%20ok%0Aobj%20%3D%20FooProperty()%20%20%20%23%20ok%0Aobj%20%3D%20FooClassVar%20%20%20%20%20%23%20ok%0Aobj%20%3D%20FooClassVar()%20%20%20%23%20ok%0Aobj%20%3D%20FooDescriptor()%20%23%20ok%0Aobj%20%3D%20FooPropertyCovariant()%20%23%20ok%0Aobj%20%3D%20FooInvalid()%20%20%20%20%23%20err%0Aobj%20%3D%20NoFoo()%20%20%20%20%20%20%20%20%20%23%20err%0Aobj%20%3D%20None%20%20%20%20%20%20%20%20%20%20%20%20%23%20err%0A%0A%0A%23%20explicit%20impl%0A%0A%0Aclass%20FooAttributeImpl(HasFoo)%3A%0A%20%20%20%20foo%3A%20int%0A%0Aclass%20FooPropertyImpl(HasFoo)%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooClassVarImpl(HasFoo)%3A%0A%20%20%20%20foo%3A%20ClassVar%5Bint%5D%20%3D%200%0A%0Aclass%20FooDescriptorImpl(HasFoo)%3A%0A%20%20%20%20%40cached_property%0A%20%20%20%20def%20foo(self)%20-%3E%20int%3A%20return%200%0A%0Aclass%20FooPropertyCovariantImpl(HasFoo)%3A%0A%20%20%20%20%40property%0A%20%20%20%20def%20foo(self)%20-%3E%20bool%3A%20return%20False%0A%0Aclass%20FooInvalidImpl(HasFoo)%3A%0A%20%20%20%20foo%3A%20str%0A%0A%40final%0Aclass%20NoFooImpl(HasFoo)%3A%0A%20%20%20%20bar%3A%20str%0A>`_
|
||||
|
||||
.. [#final_mutability]
|
||||
As noted above the second-to-last code example of https://typing.readthedocs.io/en/latest/spec/qualifiers.html#semantics-and-examples
|
||||
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document is placed in the public domain or under the
|
||||
CC0-1.0-Universal license, whichever is more permissive.
|
Loading…
Reference in New Issue