If you’re interested in playing with the ReadOnly
mechanism proposed above, I’ve added support for it in the latest version of pyright (1.1.310). I find that it’s easier to evaluate a new language or typing feature if I can experiment with it in a code editor.
I am planning on raising a new PEP for the ReadOnly special form, rather than modifying PEP 705, as it will be a significant rewrite. At that point I will withdraw the TypedMapping PEP, unless there is someone who wants it deferred?
Will this new PEP propose ReadOnly
for protocol attributes in addition to TypedDict
?
Can’t you use properties for that?
I believe you can use properties now, but this would be a shorter way to express it. Dataclass fields also got mentioned, but I would expect that to result in modification being impossible at runtime, i.e. it should use Final rather than ReadOnly? I would mildly prefer to leave extensions to follow-up PEPs since there’s quite a lot going on already in this one.
properties carry a distinct typecheck and runtime limitation.
If you explicitly subclass from a Protocol that declares a property, then it cannot be a plain instance attribute in the subclass.
Explicit subclassing is a documented use-case in the PEP: PEP 544 – Protocols: Structural subtyping (static duck typing) | peps.python.org
from __future__ import annotations
from abc import abstractmethod
from typing import Protocol
def accepts_foo(foo: Foo):
foo.bar
class Foo(Protocol):
@property
@abstractmethod
def bar(self) -> int:
...
class ImplicitImplementation:
def __init__(self):
self.bar = 123
class ExplicitImplementation(Foo):
def __init__(self):
self.bar = 123 # <-- I get a type error. How to avoid? `ExplicitImplementation` should have write-able `bar`
accepts_foo(ImplicitImplementation())
accepts_foo(ExplicitImplementation())
Additional Discord context in case it helps me to refer back to: Discord
Beginners are only allowed to post 2x links, so putting the 3rd in an additional post, additional Discord context: Discord
I personally consider that a bug. A class with an instance attribute is a valid structural subtype; it should be valid to directly subclass too. I would rather fix that bug than try to work around it with ReadOnly. Indeed, I think you would be forced to fix that bug anyway, or else you’d have the reverse problem – a class with a property couldn’t subclass a Protocol with a ReadOnly attribute. (Though I haven’t checked this direction.)
Of course if someone who’d tried to do it said it would be much easier to implement ReadOnly attributes than fix the property-in-Protocol bug, I’d happily reconsider
Hm, should that example be a bug? The protocol describes a property and I don’t know that changing that be an attribute is necessarily a faithful implementation. You can create a subclass with a writeable bar
by adding a getter (and storing the value in _bar
or something)
class ExplicitImplementation(Foo):
def __init__(self):
self._bar = 123
@property
def bar(self) -> int:
return self._bar
@bar.setter
def bar(self, value):
self._bar = value
But I don’t think there’s a way for Foo
to say “don’t implement a setter for this”, so there’s still a case for ReadOnly
in some form
Hm, should that example be a bug?
As I said, it’s a valid structural subtype. That should be the only consideration to whether it is a faithful implementation, I think. As far as structural typing goes, an attribute is just a mutable property.
Maybe I’m thinking more about the trickiness of implementation…how would Python tell the difference between “I want this to be an attribute now” versus “I forgot to implement the property (including a setter)” [1]. And it must do that in a way that doesn’t allow read-only properties to be overwritten accidentally, since I don’t think __init__
gets any special privileges.
-
which maybe would be more common? ↩︎
Understood. Perhaps the line “In this case a class could use default implementations of protocol members” in the PEP makes this impossible.
it must do that in a way that doesn’t allow read-only properties to be overwritten accidentally
I guess this is the crux of the issue: is a property in a Protocol meant to (a) prevent overwriting in a subclass, (b) provide a default implementation, or (c) is it just meant to specify that a subclass must allow read access to an attribute/property? Currently there’s no way to do (c) unambiguously AFAIK, while (a) can be done with a Final attribute. Further, (a) is not the interpretation used by structural subtyping.
As such, I would expect b+c to be the valid interpretations of a property declaration in a Protocol even when subclassing, and if a subclass redeclares it as an attribute, that should remove the default implementation.
This might be awkward to implement, but I still consider it a bug that the current code raises an error instead. Low confidence though.
I don’t think
__init__
gets any special privileges.
Are you thinking of the case where the subclass doesn’t explicitly declare an attribute, but still attempts to write it in the __init__
method? I hadn’t thought about this.
I can see more motivation now for marking an attribute ReadOnly, but it’s still worryingly complex. A separate PEP still seems like the right route to me.
But that said, I’m not aware of a PEP that changes only how type checkers work, without changing any core Python. Maybe it’s best to get this all hammered out in one go? Would value insight from PEP experts.
But that said, I’m not aware of a PEP that changes only how type checkers work, without changing any core Python. Maybe it’s best to get this all hammered out in one go? Would value insight from PEP experts.
PEP 692 basically fits that description (though its implementation did end up changing the repr()
of Unpack
).
But I agree with you that changes to Protocol and TypedDict are best left to separate PEPs.
Are you thinking of the case where the subclass doesn’t explicitly declare an attribute, but still attempts to write it in the
__init__
method? I hadn’t thought about this.
Yeah basically. Although the example above is a little more complicated because of the abstract method–it’s erroring on instantiation, as it complains about the missing abstract method before __init__
even runs, which is guess is the answer to how __init__
is special.
Theoretically it could do that check after executing code, but I bet that would break a ton of stuff.
On the other hand, this (cursed?) version works:
class ExplicitImplementation(Foo):
bar = None
def __init__(self):
self.bar = 123
accepts_foo(ExplicitImplementation()) # no problem
Getting back to the topic of the new TypedMapping PEP (705)…
I am planning on raising a new PEP for the ReadOnly special form, rather than modifying PEP 705, as it will be a significant rewrite. At that point I will withdraw the TypedMapping PEP, unless there is someone who wants it deferred?
If you’re not planning to continue PEP 705 (TypedMapping) yourself I think it would be appropriate to Withdraw it (rather than to Defer it).
FWIW, I do think functionality of a “TypedMapping” - an immutable TypedDict - would be generally useful. However I don’t currently have the bandwidth to attempt to continue the design/implementation of such a PEP myself at this time.
If you’re interested in playing with the
ReadOnly
mechanism proposed above, I’ve added support for it in the latest version of pyright (1.1.310).
Thanks @erictraut ! This will be helpful for if/when someone picks up the TypedMapping PEP again.
I agree with you that changes to Protocol and TypedDict are best left to separate PEPs.
Strongly agree. +1
I’m not aware of a PEP that changes only how type checkers work, without changing any core Python. […] Would value insight from PEP experts.
There are several PEPs that mainly alter how type checkers work while making minimal other changes to Python, other than perhaps adding members to the typing
module. Some examples from an article I wrote last year:
other than perhaps adding members to the
typing
module
I was considering this part of “core Python”
I do think functionality of a “TypedMapping” - an immutable TypedDict - would be generally useful.
The new PEP I’m drafting will have “immutable” TypedDicts, just not the TypedMapping protocol. It will look like
class Record(TypedDict, read_only=True):
a: str
(Scare quotes around immutable because it’s only this structural type that lacks mutability. The underlying dict could still have other references that can change it.)