You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
non-final frozen attributes (was: Improved read-only attributes of Protocol) #922
Are there any ideas about making read-only attributes in Protocol easier to define? Immutable objects (e.g. datetime) and frozen dataclasses are great, but they are painful to integrate with Protocol. Which is a shame, because immutability and structural typing are both great!
The problem
classSomeProto(Protocol):
a: intb: str@dataclass(frozen=True)classSomeDClass:
a: int=1b: str=""c: bool=False# not allowed as attributes must also be settable to satisfy protocolx: SomeProto=SomeDClass()
# to allow non-settable attributes, we have to define this.# It's 3 times as long, and less readableclassSomeProto2(Protocol):
@propertydefa(self) ->int:
...
@propertydefb(self) ->str:
...
# ...but it does worky: SomeProto2=SomeDClass()
Why do something about it
In my experience, the use case for purely read-only attributes is far more common than settable attributes. It feels strange that this requires such a more cumbersome and less readable workaround. This can be easily fixed without breaking existing behavior.
Proposed solution: allow using Final or another annotation
Currently this gives an explicit error (error: Protocol member cannot be final). Understandable, because the semantics don't quite match up.
However, PEP591 does seem to leave the door open a bit:
The current typing module lacks a way to indicate that a variable will not be assigned to. This is a useful feature in several situations:
[...]
Creating a read-only attribute that may not be overridden by subclasses. (@property can make an attribute read-only but does not prevent overriding)
An alternative would be a separate ReadOnly[...] annotation or similar
What are your thoughts?
One usecase that complicates things is adding mutability through inheritance:
classAddress(Protocol):
street: Final[str] # only readablezipcode: Final[int]
classMutableAddress(Address):
street: str# readable and writablezipcode: int
What are your thoughts on this?
Going from 'frozen' to non-'frozen' through inheritance
is something Python often avoids (frozenset/set, dataclass)
If we want to support this, Final would be the wrong choice.
Other options I can think of:
It seems the problem is more general: could we have a dedicated way to prevent reassignment of attributes without also preventing overriding (which Final does)?
The problem
The goals of Final as stated in PEP 591 are to support:
Declaring that a method should not be overridden
Declaring that a class should not be subclassed
Declaring that a variable or attribute should not be reassigned
The problem is that (1) limits (3). Consider the following:
classMeasurement:
timestamp: Final[date] # final used here to prevent reassigning this attributevalue: intdef__init__(self, value: int):
self.timestamp=date.today()
self.value=valuem=Measurement(0)
m.timestamp=date(2020, 1, 1) # this is prevented, as it should beclassAccurateMeasurement(Measurement):
# Declaring this attribute is currently not allowed because Final forbids overriding `timestamp`.# If we're only interested in preventing attribute reassignment, it should be possible though.timestamp: Final[datetime] # (note: datetime is a subclass of date)def__init__(self, value: int):
self.timestamp=datetime.now()
self.value=value
Proposal
An annotation specifically for preventing reassignment of attributes, but that does not prevent overriding.
It could be called something like Frozen.
An additional advantage is that readonly members of protocols become way easier to define:
Well, technically you override timestamp in Measurement with AccurateMeasurement:
classAccurateMeasurement(Measurement):
# Declaring this attribute is currently not allowed because Final forbids overriding `timestamp`# If we're only interested in preventing attribute reassignment, it should be possible though.timestamp: Final[datetime] # note: datetime is a subclass of datedef__init__(self, value: int):
self.timestamp=datetime.now()
self.value=value
It is just made via subclassing that reassignes timestamp in a different way.
This will be especially visiable when super().__init__() is called.
changed the title [-]Improved read-only attributes of Protocol[/-][+]non-final frozen attributes (was: Improved read-only attributes of Protocol)[/+]on Nov 25, 2021
It is just made via subclassing that reassignes timestamp in a different way.
Note that PEP591 does seem to imply 'reassignment' of attributes means: assignment outside __init__. (Would subclassing with a different __init__ count as 'reassignment' 🤔 ?)
In any case, the rule for Frozen attributes would be: can only be set in __init__.
@ilevkivskyi I was wondering if you had any thoughts on the matter -- since you co-authored PEP591 and wrote the reference implementation for Final in mypy?
IMO the convenience of saving one line of code (by not using a property) is not worth introducing some special rules/concepts. IIRC final things were developed explicitly to not overlap with existing solutions (like properties).
Thanks @srittau@ilevkivskyi for the remarks! I can definitely understand not wanting to add
an extra overlapping concept for the use case of specifying read-only attributes.
To answer your question, I'd say there are 2 limitations of property (for specifying a read-only attribute)
Limitation 1: properties in Protocols and ABCs are so verbose that they obscure intention
classMeasurementLike(Protocol):
@propertydeftimestamp(self) ->date:
...
classMeasurementBase(ABC):
@property@abstractmethoddeftimestamp(self) ->date:
...
# compare with the much less noisyclassMeasurementLike(Protocol):
timestamp: Final[date]
Does this occur often enough to warrant a shorthand?
A quick search of the typeshed repo shows circa 1 in 6 (!) defs are a property.
A good shorthand would thus cut out a lot of noise! Readability counts 👀
Potential solution to limitation 1: a type qualifier for property
A simple solution would be a Property type qualifier:
classMeasurementLike(Protocol):
timestamp: Property[date]
classMeasurementBase(ABC):
timestamp: Property[date] # no implementation means it's abstract
We could also simply allow property itself to function as a type qualifier,
to cut down on imports.
Limitation 2: In concrete classes, property requires a lot of extra moving parts.
The example below shows the noise needed for one read-only attribute:
It takes a while to understand "aha! All this is to make timestamp read-only"
Potential solution to limitation 2: a helper for "assign once" properties
In addition to the type qualifier, a new descriptor could implement the common 'assign once' pattern:
classMeasurement:
timestamp: Property[date] =property.assigned_oncedef__init__(self):
self.timestamp= ... # assignment allowed herem=Measurement()
m.timestamp= ... # a subsequent assignment is blocked at runtime
Do we need an extra helper? In 3.8 cached_property was added to functools. The 'assign once' behavior is IMO at least as common as caching, if not more.
Of course in this case you could use Final, but you wouldn't be able to set the attribute in a subclass __init__.
Also, an advantage of descriptors could be allowing other behaviors such as allowing
reassignment only privately.
tl;dr
property is the preferred way to model read-only attributes,
but it can be clunky to use in the simplest and most common cases, hindering readability.
A Property type qualifier and a new descriptor can give us a readable shorthand
without having to resort to new concepts.
FYI I created a small mypy plugin for fixing the initial problem of read-only protocols: link
Nevertheless I'd be interested to hear any thoughts on the Property type qualifier described above.
If it's not deemed a good idea, we can close this issue ✅
Activity
ariebovenberg commentedon Nov 17, 2021
☝️ NOTE
I have since refined this issue, see #922 (comment)
Below I have kept the original text
One usecase that complicates things is adding mutability through inheritance:
What are your thoughts on this?
Going from 'frozen' to non-'frozen' through inheritance
is something Python often avoids (
frozenset/set,dataclass)If we want to support this,
Finalwould be the wrong choice.Other options I can think of:
ReadOnlyReadFrozenGettableGetImmutableariebovenberg commentedon Nov 25, 2021
It seems the problem is more general: could we have a dedicated way to prevent reassignment of attributes without also preventing overriding (which
Finaldoes)?The problem
The goals of
Finalas stated in PEP 591 are to support:The problem is that (1) limits (3). Consider the following:
Proposal
An annotation specifically for preventing reassignment of attributes, but that does not prevent overriding.
It could be called something like
Frozen.An additional advantage is that readonly members of protocols become way easier to define:
instead of
sobolevn commentedon Nov 25, 2021
Well, technically you override
timestampinMeasurementwithAccurateMeasurement:It is just made via subclassing that reassignes
timestampin a different way.This will be especially visiable when
super().__init__()is called.ariebovenberg commentedon Nov 25, 2021
@sobolevn you could indeed interpret it that way 🤔. Maybe it's better to talk of 'frozen' attributes, like with dataclasses:
(the following code checks out)
it'd be useful to express 'frozenness' of attributes outside of dataclasses:
[-]Improved read-only attributes of Protocol[/-][+]non-final frozen attributes (was: Improved read-only attributes of Protocol)[/+]ariebovenberg commentedon Nov 25, 2021
Note that PEP591 does seem to imply 'reassignment' of attributes means: assignment outside
__init__. (Would subclassing with a different__init__count as 'reassignment' 🤔 ?)In any case, the rule for
Frozenattributes would be: can only be set in__init__.ariebovenberg commentedon Nov 26, 2021
additionally, a
@frozendecorator may be useful to indicate a class is fully frozen:A lot more readable as the number of attributes increases!
ariebovenberg commentedon Dec 2, 2021
Found a related discussion about read-only protocol members: #903 (conclusion was use the
@propertysyntax)ariebovenberg commentedon Dec 2, 2021
@ilevkivskyi I was wondering if you had any thoughts on the matter -- since you co-authored PEP591 and wrote the reference implementation for
Finalinmypy?ilevkivskyi commentedon Dec 3, 2021
IMO the convenience of saving one line of code (by not using a property) is not worth introducing some special rules/concepts. IIRC final things were developed explicitly to not overlap with existing solutions (like properties).
srittau commentedon Dec 3, 2021
What use case isn't covered by
@property?ariebovenberg commentedon Dec 3, 2021
Thanks @srittau @ilevkivskyi for the remarks! I can definitely understand not wanting to add
an extra overlapping concept for the use case of specifying read-only attributes.
To answer your question, I'd say there are 2 limitations of
property(for specifying a read-only attribute)Limitation 1: properties in Protocols and ABCs are so verbose that they obscure intention
Does this occur often enough to warrant a shorthand?
A quick search of the
typeshedrepo shows circa 1 in 6 (!)defs are aproperty.A good shorthand would thus cut out a lot of noise! Readability counts 👀
Potential solution to limitation 1: a type qualifier for property
A simple solution would be a
Propertytype qualifier:We could also simply allow
propertyitself to function as a type qualifier,to cut down on imports.
Limitation 2: In concrete classes,
propertyrequires a lot of extra moving parts.The example below shows the noise needed for one read-only attribute:
It takes a while to understand "aha! All this is to make
timestampread-only"Potential solution to limitation 2: a helper for "assign once" properties
In addition to the type qualifier, a new descriptor could implement the common 'assign once' pattern:
Do we need an extra helper? In 3.8
cached_propertywas added tofunctools. The 'assign once' behavior is IMO at least as common as caching, if not more.Of course in this case you could use
Final, but you wouldn't be able to set the attribute in a subclass__init__.Also, an advantage of descriptors could be allowing other behaviors such as allowing
reassignment only privately.
tl;dr
propertyis the preferred way to model read-only attributes,but it can be clunky to use in the simplest and most common cases, hindering readability.
Propertytype qualifier and a new descriptor can give us a readable shorthandwithout having to resort to new concepts.
ariebovenberg commentedon Dec 13, 2021
FYI I created a small mypy plugin for fixing the initial problem of read-only protocols: link
Nevertheless I'd be interested to hear any thoughts on the
Propertytype qualifier described above.If it's not deemed a good idea, we can close this issue ✅
ariebovenberg commentedon Dec 20, 2021
I see that this particular
Propertyidea been proposed before: #594I will proceed to close this issue 🔒