microsoft / pyright

Static Type Checker for Python
Other
13.12k stars 1.4k forks source link

Improve detection of possible runtime AttributeError caused by accessing uninitialized instance or class member #8680

Closed aholmes closed 1 month ago

aholmes commented 1 month ago

Is your feature request related to a problem? Please describe. This is related to #6938 and is a requested improvement for this specific issue #3886.

With this code

class Foo:
    x: int

Foo().x raises an AttributeError:

'Foo' object has no attribute 'x'

Foo.x raises and `AttributeError:

AttributeError: type object 'Foo' has no attribute 'x'

Describe the solution you’d like

As has already been referenced, PEP526 states:

In particular, the value-less notation a: int allows one to annotate instance variables that should be initialized in init or new.

My request is two-part:

I interpret the intent of this to mean any accesses of a class member is an error regardless of Python code's ability to set Foo.x through other means. So the first part of my request is that Foo.x should be flagged, and the member should not show up in any "intellisense" (I think this last part is pylance, though). I believe this position is bolstered by the existence of ClassVar[T], which is intended to indicate that a class member is in fact a class member. This implies that any "class member"-looking declarations not declared with ClassVar[T] are in fact instance members. To me, this means the by-design concerns mentioned in #3886 can be "short-circuited" by avoiding what is possible in Python and instead taking the stance that what is declared is what is intended and correct, and limiting Pyright's "responsibility" to what it can determine about this class and its usage.

Here's an example:

>>> class Foo:
...     x: int
...
...     def __init__(self):
...         super().__init__()
...         self.x = 1
...
>>> Foo().x
1
>>> Foo.x
Traceback (most recent call last):
  File "<stdin>", line 9, in <module>
AttributeError: type object 'Foo' has no attribute 'x'

Nothing in this code's declaration indicates that Foo.x is valid, but Pyright does not indicate the possibility of an issue.

$ PYRIGHT_PYTHON_FORCE_VERSION=latest pyright foo.py
0 errors, 0 warnings, 0 informations

Similarly, Pyright does not error with self.x, but calling Foo().func() is a runtime error:

class Foo:
    x: int

    def func(self):
        self.x

@erictraut mentioned this:

Pyright does offer a super-strict diagnostic rule called reportUninitializedInstanceVariable. This rule isn't even enabled in strict mode because it imposes a bunch of additional restrictions on how you must write your code. But with those restrictions and assumptions pyright will catch the error you mentioned. Try that and see if it suits your needs.

This does indeed catch if the instance member is not assigned, but it does not affect the class member access that causes a runtime error.

The second part of my request regards usage of ClassVar:

class Bar:
    x: ClassVar[int]

Bar.x

I wonder if there is some kind of flagging that can be done here despite Python's ability to set x from anywhere. Having the ability to flag this as a warning could go a long way toward catching possible errors, and gives developers the opportunity to ignore a flag when they know it's not an error.


I understand implementing this request is complex due to how Python works, and the likelihood of causing problems in existing codebases. I am hoping there is some form of limited type checking that can occur in these cases so that developers might have the option to decide for themselves how to handle flagging of these issues in their code.

For your reference, here is my Pyright configuration.

erictraut commented 1 month ago

Please refer to this documentation for details about how pyright interprets class and instance variables.

If you want x to be treated as a "pure" instance variable — one that cannot have a class variable backing it, then you should declare the variable in the __init__ method or some other instance method.

class Foo:
    def func(self):
        self.x: int

Foo.x  # Error