PEP 705 introduced thetyping.ReadOnly type qualifierto allow defining read-onlytyping.TypedDict items.
This PEP proposes usingReadOnly inannotations of class and protocolattributes, as a single concise way to mark them read-only.
Akin toPEP 705, it makes no changes to setting attributes at runtime. Correctusage of read-only attributes is intended to be enforced only by static type checkers.
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 asC#orTypeScript),and is useful for removing the ability to reassign ordelete an attributeat a type checker level, as well as defining a broad interface for structural subtyping.
Today, there are three major ways of achieving read-only attributes, honored by type checkers:
typing.Final:classFoo:number:Final[int]def__init__(self,number:int)->None:self.number=numberclassBar:def__init__(self,number:int)->None:self.number:Final=number
dataclasses (and type checkers sincetyping#1669).number is not possible - the specification ofFinalimposes that the name cannot be overridden in subclasses.@property:classFoo:_number:intdef__init__(self,number:int)->None:self._number=number@propertydefnumber(self)->int:returnself._number
number is possible.Type checkers disagree about the specific rules.[1]dataclasses, but does not compose well - the synthesized__init__ and__repr__ will use_number as the parameter/attribute name.dataclasses.dataclass() ortyping.NamedTuple:@dataclass(frozen=True)classFoo:number:int# implicitly read-onlyclassBar(NamedTuple):number:int# implicitly read-only
number is possible in the@dataclass case.NamedTuple is still atuple. Most classes do not need to inheritindexing, iteration, or concatenation.Suppose aProtocol membername:T defining two requirements:
hasattr(obj,"name")isinstance(obj.name,T)Those requirements are satisfiable at runtime by all of the following:
name:T,name:ClassVar[T],@propertydefname(self)->T,functools.cached_property().The currenttyping specallows creation of such protocol members using (abstract) properties:
classHasName(Protocol):@propertydefname(self)->T:...
This syntax has several drawbacks:
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 inPEP 705.
A class with a read-only instance attribute can now be defined as:
fromtypingimportReadOnlyclassMember:def__init__(self,id:int)->None:self.id:ReadOnly[int]=id
…and the protocol described inProtocols is now just:
fromtypingimportProtocol,ReadOnlyclassHasName(Protocol):name:ReadOnly[str]defgreet(obj:HasName,/)->str:returnf"Hello,{obj.name}!"
Member can redefine.id as a writable attribute or adescriptor. It can alsonarrow the type.HasName protocol has a more succinct definition, and is agnosticto the writability of the attribute.greet function can now accept a wide variety of compatible objects,while being explicit about no modifications being done to the input.Thetyping.ReadOnlytype qualifierbecomes a valid annotation forattributes of classes and protocols.It can be used at class-level or within__init__ to mark individual attributes read-only:
classBook:id:ReadOnly[int]def__init__(self,id:int,name:str)->None:self.id=idself.name:ReadOnly[str]=name
Type checkers should error on any attempt to reassign ordelete an attributeannotated withReadOnly.Type checkers should also error on any attempt to delete an attribute annotated asFinal.(This is not currently specified.)
Use ofReadOnly in annotations at other sites where it currently has no meaning(such as local/global variables or function parameters) is considered out of scopefor this PEP.
Akin toFinal[4],ReadOnly does not influence howtype checkers perceive the mutability of the assigned object. ImmutableABCsandcontainers may be used in combination withReadOnlyto forbid mutation of such values at a type checker level:
fromcollectionsimportabcfromdataclassesimportdataclassfromtypingimportProtocol,ReadOnly@dataclassclassGame:name:strclassHasGames[T:abc.Collection[Game]](Protocol):games:ReadOnly[T]defadd_games(shelf:HasGames[list[Game]])->None:shelf.games.append(Game("Half-Life"))# ok: list is mutableshelf.games[-1].name="Black Mesa"# ok: "name" is not read-onlyshelf.games=[]# error: "games" is read-onlydelshelf.games# error: "games" is read-only and cannot be deleteddefread_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-onlyshelf.games=[]# error: "games" is read-only
All instance attributes of frozen dataclasses andNamedTuple should beimplied to be read-only. Type checkers may inform that annotating such attributeswithReadOnly is redundant, but it should not be seen as an error:
fromdataclassesimportdataclassfromtypingimportNewType,ReadOnly@dataclass(frozen=True)classPoint:x:int# implicit read-onlyy:ReadOnly[int]# ok, redundantuint=NewType("uint",int)@dataclass(frozen=True)classUnsignedPoint(Point):x:ReadOnly[uint]# ok, redundant; narrower typey:Final[uint]# not redundant, Final imposes extra restrictions; narrower type
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.Depending on the kind of the attribute, they can be assigned to at different sites:
Assignment to an instance attribute must be allowed in the following contexts:
__init__, on the instance received as the first parameter (likely,self).__new__, on instances of the declaring class created via a callto a super-class’__new__ method.Additionally, a type checker may choose to allow the assignment:
__new__, on instances of the declaring class, without regardto the origin of the instance.(This choice trades soundness, as the instance may already be initialized,for the simplicity of implementation.)@classmethods, on instances of the declaring class created viaa call to the class’ or super-class’__new__ method.fromcollectionsimportabcfromtypingimportReadOnlyclassBand:name:strsongs:ReadOnly[list[str]]def__init__(self,name:str,songs:abc.Iterable[str]|None=None)->None:self.name=nameself.songs=[]ifsongsisnotNone:self.songs=list(songs)# multiple assignments are finedefclear(self)->None:# error: assignment to read-only "songs" outside initializationself.songs=[]band=Band(name="Bôa",songs=["Duvet"])band.name="Python"# ok: "name" is not read-onlyband.songs=[]# error: "songs" is read-onlyband.songs.append("Twilight")# ok: list is mutableclassSubBand(Band):def__init__(self)->None:self.songs=[]# error: cannot assign to a read-only attribute of a base class
# a simplified immutable Fraction classclassFraction: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)ifdenominatorisNone:iftype(numerator)isint:self.numerator=numeratorself.denominator=1returnselfelifisinstance(numerator,Rational):...else:...@classmethoddeffrom_float(cls,f:float,/)->Self:self=super().__new__(cls)self.numerator,self.denominator=f.as_integer_ratio()returnself
Read-only class attributes are attributes annotated as bothReadOnly andClassVar.Assignment to such attributes must be allowed in the following contexts:
__init_subclass__, on the class object received as the first parameter (likely,cls).classURI:protocol:ReadOnly[ClassVar[str]]=""def__init_subclass__(cls,protocol:str="")->None:cls.protocol=protocolclassFile(URI,protocol="file"):...
When a class-level declaration has an initializing value, it can serve as aflyweightdefault for instances:
classPatient:number:ReadOnly[int]=0def__init__(self,number:int|None=None)->None:ifnumberisnotNone:self.number=number
Note
This feature conflicts with__slots__. An attribute witha 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 uninitializedafter an instance is created (except instubs,protocols or ABCs):
classPatient:id:ReadOnly[int]# error: "id" is not initialized on all code pathsname:ReadOnly[str]# error: "name" is never initializeddef__init__(self)->None:ifrandom.random()>0.5:self.id=123classHasName(Protocol):name:ReadOnly[str]# ok
The inability to reassign read-only attributes makes them covariant.This has a few subtyping implications. Borrowing fromPEP 705:
@dataclassclassHasTitle:title:ReadOnly[str]@dataclassclassGame(HasTitle):title:stryear:intgame=Game(title="DOOM",year=1993)game.year=1994game.title="DOOM II"# ok: attribute is not read-onlyclassTitleProxy(HasTitle):@functools.cached_propertydeftitle(self)->str:...classSharedTitle(HasTitle):title:ClassVar[str]="Still Grey"
classGame(HasTitle):year:intdef__init__(self,title:str,year:int)->None:super().__init__(title)self.title=title# error: cannot assign to a read-only attribute of base classself.year=yeargame=Game(title="Robot Wants Kitty",year=2010)game.title="Robot Wants Puppy"# error: "title" is read-only
classGameCollection(Protocol):games:ReadOnly[abc.Collection[Game]]@dataclassclassGameSeries(GameCollection):name:strgames:ReadOnly[list[Game]]# ok: list[Game] is assignable to Collection[Game]
classMyBase(abc.ABC):foo:ReadOnly[int]bar:ReadOnly[str]="abc"baz:ReadOnly[float]def__init__(self,baz:float)->None:self.baz=baz@abstractmethoddefpprint(self)->None:...@finalclassMySubclass(MyBase):# error: MySubclass does not override "foo"defpprint(self)->None:print(self.foo,self.bar,self.baz)
name:ReadOnly[T] indicates that a structuralsubtype must support.name access, and the returned value is assignable toT:classHasName(Protocol):name:ReadOnly[str]classNamedAttr:name:strclassNamedProp:@propertydefname(self)->str:...classNamedClassVar:name:ClassVar[str]classNamedDescriptor:@cached_propertydefname(self)->str:...# all of the following are okhas_name:HasNamehas_name=NamedAttr()has_name=NamedProp()has_name=NamedClassVarhas_name=NamedClassVar()has_name=NamedDescriptor()
ReadOnly can be used withClassVar andAnnotated in any nesting order:
classFoo:foo:ClassVar[ReadOnly[str]]="foo"bar:Annotated[ReadOnly[int],Gt(0)]
classFoo:foo:ReadOnly[ClassVar[str]]="foo"bar:ReadOnly[Annotated[int,Gt(0)]]
This is consistent with the interaction ofReadOnly andtyping.TypedDictdefined inPEP 705.
An attribute cannot be annotated as bothReadOnly andFinal, as the twoqualifiers differ in semantics, andFinal is generally more restrictive.Final remains allowed as an annotation of attributes that are only impliedto be read-only. It can be also used to redeclare aReadOnly attribute of a base class.
This PEP introduces new contexts whereReadOnly is valid. Programs inspectingthose places will have to change to support it. This is expected to mainly affect type checkers.
However, caution is advised while using the backportedtyping_extensions.ReadOnlyin older versions of Python. Mechanisms inspecting annotations may behave incorrectlywhen encounteringReadOnly; in particular, the@dataclass decoratorwhichlooks forClassVar may mistakenly treatReadOnly[ClassVar[...]] as an instance attribute.
To avoid issues with introspection, useClassVar[ReadOnly[...]] instead ofReadOnly[ClassVar[...]].
There are no known security consequences arising from this PEP.
Suggested changes to thetyping module documentation,following the footsteps ofPEP 705:
typing.ReadOnly to this PEP.typing.ReadOnly:A special typing construct to mark an attribute of a class or an item ofaTypedDictas read-only.
ReadOnly under thetype qualifiers section:TheReadOnlytype qualifier in class attribute annotations indicatesthat the attribute of the class may be read, but not reassigned ordeleted.For usage inTypedDict, seeReadOnly.
@property and ProtocolsTheProtocols section mentions an inconsistency between type checkers inthe interpretation of properties in protocols. The problem could be fixedby amending the typing specification, clarifying what implements the read-onlyquality of such properties.
This PEP makesReadOnly a better alternative for defining read-only attributesin protocols, superseding the use of properties for this purpose.
__init__ and Class BodyAn earlier version of this PEP proposed that read-only attributes could only beassigned to in__init__ and the class’ body. A later discussion revealed thatthis restriction would severely limit the usability ofReadOnly withinimmutable classes, which typically do not define__init__.
fractions.Fraction is one example of an immutable class, where theinitialization of its attributes happens within__new__ and classmethods.However, unlike in__init__, the assignment in__new__ and classmethodsis potentially unsound, as the instance they work on can be sourced froman arbitrary place, including an already finalized instance.
We find it imperative that this type checking feature is useful to the foremostuse site of read-only attributes - immutable classes. Thus, the PEP has changedsince to allow assignment in__new__ and classmethods under a set of rulesdescribed in theInitialization section.
Mechanisms such asdataclasses.__post_init__() or attrs’initialization hooksaugment object creation by providing a set of special hooks which are calledduring initialization.
The current initialization rules defined in this PEP disallow assignment toread-only attributes in such methods. It is unclear whether the rules could besatisfyingly shaped in a way that is inclusive of those 3rd party hooks, whileupkeeping the invariants associated with the read-only-ness of those attributes.
The Python type system has a long and detailedspecificationregarding the behavior of__new__ and__init__. It is rather unfeasibleto expect the same level of detail from 3rd party hooks.
A potential solution would involve type checkers providing configuration in thisregard, requiring end users to manually specify a set of methods they wishto allow initialization in. This however could easily result in users mistakenlyor purposefully breaking the aforementioned invariants. It is also a fairlybig ask for a relatively niche feature.
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0767.rst
Last modified:2025-05-06 20:54:28 GMT