The difference is that pyright currently would reject that code without ReadOnly:
$ mypy q.py
q.py:16: note: Revealed type is "builtins.bool"
Success: no issues found in 1 source file
$ pyright q.py
WARNING: there is a new pyright version available (v1.1.403 -> v1.1.405).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
~/q.py
~/q.py:11:5 - error: "x" overrides symbol of same name in class "B"
Variable is mutable so its type is invariant
Override type "bool" is not the same as base type "int" (reportIncompatibleVariableOverride)
~/q.py:16:13 - information: Type of "c.x" is "bool"
1 error, 0 warnings, 1 information
With a little modification both mypy and pyright will accept it though:
from typing import reveal_type
class B:
def __init__(self, x: int) -> None:
self.x = x
def foo(self, x: int) -> None:
self.x = x
class C(B):
def __init__(self, x: bool = True) -> None:
self.x = x
c: C = C()
c.foo(4)
reveal_type(c.x)
assert isinstance(c.x, bool)
With pyright the types are handled differently for attributes that are assigned in __init__ vs those that are declared as attributes explicitly in the class body. Personally I find this quite annoying because I want the type checker behaviour seen here with __init__ but I want to declare the attributes in the class body. In general I prefer annotating the attributes in the class body but also it is necessary when using __new__ rather than __init__.
It seems clear that pyright makes this __init__ exception because it is known how many false positives it would otherwise generate but I see these false positives all the time. The primary reason I want ReadOnly is in fact precisely to tell pyright to silence the error shown above. Of course it is useful to be able to signal to other code not to mutate the attribute but in practice I don’t find that other code mutating attributes that are not supposed to be mutated is a real problem.
Additionally I want to be able to mix class attributes, instance attributes and properties when ReadOnly and this is something that mypy and pyright both reject:
from typing import Literal
class A:
is_thing: bool = False
class B(A):
# mypy and pyright don't like this:
@property
def is_thing(self) -> bool:
return True
class C(A):
def __init__(self, is_thing: bool = False):
self.is_thing = is_thing
a: A = B()
if a.is_thing:
print("a is a thing")
Would the PEP allow that to work with is_thing: ReadOnly[bool]?
There are all kinds of cases like this where it is possible for a subclass to do something weird but worrying about that is misplaced. Right now it is not possible to put correct type annotations on correct code and have the type checkers agree that the code is correct. That is the primary thing that needs addressing.
Type checkers are most useful at checking things at the boundaries like function signatures and the class interface rather than checking the internals of the class. There should be some way to say “instances of A have a readable attribute is_thing” and the need for that is far more important than having type checkers try (inevitably in vain) to guarantee that no (often hypothetical) subclass could ever do something strange with the attribute.