I welcome a standardized way to differentiate between a “pure instance variable” and a “class+instance variable” (in stubs and elsewhere), but the proposed mechanism is still not entirely clear to me.
Leaving aside stubs for a moment, let’s consider the following sample.
class A:
x: int = 0
y: int
def __init__(self) -> None:
self.z: int = 0
@classmethod
def init_me(cls) -> None:
cls.y = 0
Which of the following subsequent statements would you expect to be flagged as a type error?
A.x = 1
A.y = 1
A.z = 1
Mypy is fine with all three of these statements. Pyright flags the third one as an error because it considers z a “pure instance variable” — one that cannot be accessed directly on the class object. Pyright allows the first two statements because it considers x and y to be “class+instance variables”.
If I understand your proposal correctly, the statement y: int within the class body would imply that y is a “pure instance variable”. That would mean A.y = 1 would be an error — as would cls.y = 0. I’m not convinced that’s the right behavior. It would definitely be a breaking change for pyright users.
Perhaps you’re proposing that these meanings apply only to classes defined in “.pyi” files but not in “.py” files? I don’t think we should special-case stubs in this manner. I think we can agree that ascribing different meaning in stubs versus non-stubs is confusing for users. I’d prefer to focus on solutions that work consistently regardless of where a class is defined.
Here’s another edge case that needs to be considered. What if we split the declaration and the initial assignment into separate lines? By your proposal, would x and y in the sample below be a “pure instance variable” or a “class+instance variable”?
class A:
x: int
x = 0
y: int
if get_condition():
y = 0
print(A.x) # Allowed?
print(A.y) # Allowed?