Skip to content

Add a ... initial value to attributes declared on object's class #13875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

grievejia
Copy link
Contributor

@grievejia grievejia commented Apr 24, 2025

Normally, attributes that are declared at class toplevel but are uninitialized there cannot be directly accessed from the class itself at runtime. For instance,

class Foo:
  x: int
  def __init__(self, x: int) -> None:
    self.x = x

Foo.x  # Runtime crash! type object 'Foo' has no attribute 'x'
Foo().x  # This is OK -- `x` actually gets initialized within `__init__`

This makes sense because at runtime, the bare x: int "declaration" on Foo toplevel is just an annotated assignment statement without a RHS, which semantically is a no-op. Nothing actually gets added to either the class Foo or any of the Foo object. But if we try to assign an initial value to the attribute, x will be there:

class Bar:
  x: int = 42

Bar.x  # OK!
Bar().x  # OK!

Pyrefly is making an attempt at reporting an error on this kind of issue, where if an attribute gets declared at class toplevel without an initial value, we ban all accesses to the attribute from the class object.
One issue we have, when enabling the error, is that many stub files on typeshed doesn't really pay much attention to the difference between initialized and uninitialized class toplevel attribute declarations. For example, there are several declarations in object class toplevel such as __doc__ and __dict__ where the value does actually exist on the object class directly, but typeshed models them as uninitialized class toplevel attribute which causes Pyrefly to misreport when those attributes are accessed.
I don't know how far we could go with reporting this new kind of error (which neither mypy nor pyright would flag) eventually, but given that "fixing" the issue requires only trivial changes from the stub files, we may as well just try adding = ... to class-toplevel attributes that must exist at runtime, starting from the class object.


2025-04-25 (@srittau): Deferred, pending further discussion with the wider typing community.

Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

vision (https://github.com/pytorch/vision)
- torchvision/utils.py:270: error: Unused "type: ignore" comment  [unused-ignore]

@grievejia grievejia marked this pull request as ready for review April 24, 2025 03:18
@srittau
Copy link
Collaborator

srittau commented Apr 24, 2025

While I'm not necessarily opposed to this, I think this should be discussed with the wider typing community. So far in typeshed, we've treated x: int and x: int = ... not only as identical, but the latter form even as outdated. (I'm surprised that flake8-pyi doesn't check for it.) I would like to have at least some guidance/standard somewhere in the documentation about this.

@srittau
Copy link
Collaborator

srittau commented Apr 24, 2025

In fact, I don't think there's currently any way to really distinguish between these four cases in stubs:

class X:
    a: int
    b: int
    c: int = 3
    def __init__(self) -> None:
        self.b = 3
        self.d = 4

@AlexWaygood
Copy link
Member

We're also trying to detect this error in red-knot, and have also run into the issue that default values in class bodies are not generally included in stub files, unlike other Python source-code files. I'm basically in alignment with @srittau here -- I'm not necessarily opposed, but I do think some broader discussion might be good. This isn't just a typeshed-specific convention; I think most stubs outside typeshed also generally don't distinguish between instance attributes with class-level defaults and instance attributes without class-level defaults.

The workaround red-knot has been using is to consider any annotations in stub files to constitute both a binding and a declaration (whereas in a non-stub file it would only constitute a declaration, not a binding). But if we can find consensus in the community that stubs should generally prefer to indicate whether an instance attribute has a class-level default or not, that would also benefit red-knot.

@grievejia
Copy link
Contributor Author

Thank you so much for the feedback @srittau @AlexWaygood! I'll try to bring this up in the forum and follow red-knot's approach of special-casing these in stubs in the meantime.

@srittau srittau added the status: deferred Issue or PR deferred until some precondition is fixed label Apr 25, 2025
@srittau srittau marked this pull request as draft April 25, 2025 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: deferred Issue or PR deferred until some precondition is fixed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants