PEP 767 – Annotating Read-Only Attributes
- Author:
- Łukasz Modzelewski
- Sponsor:
- Carl Meyer <carl at oddbird.net>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 18-Nov-2024
- Python-Version:
- 3.15
- Post-History:
- 09-Oct-2024 05-Dec-2024
Abstract
PEP 705 introduced the typing.ReadOnly type qualifier
to allow defining read-only typing.TypedDict items.
This PEP proposes using ReadOnly in annotations of class and protocol
attributes, 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.
Terminology
This PEP uses “read-only” to describe attributes which may be read, but not assigned to (except in limited cases to support initialization) or deleted.
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# or TypeScript), and is useful for removing the ability to assign to or delete an attribute at a type checker level, as well as defining a broad interface for structural subtyping.
Classes
Today, there are three major ways of achieving read-only attributes, honored by type checkers:
- annotating the attribute with
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
dataclasses(and type checkers since typing#1669). - Overriding
numberis not possible - the specification ofFinalimposes that the name cannot be overridden in subclasses.
- Supported by
- marking the attribute “_internal”, and exposing it via read-only
@property:class Foo: _number: int def __init__(self, number: int) -> None: self._number = number @property def number(self) -> int: return self._number
- Overriding
numberis possible, but limited to using@property. [1] - Read-only at runtime. [2]
- Requires extra boilerplate.
- Supported by
dataclasses, but does not compose well - the synthesized__init__and__repr__will use_numberas the parameter/attribute name.
- Overriding
- using a “freezing” mechanism, such as
dataclasses.dataclass()ortyping.NamedTuple:@dataclass(frozen=True) class