Skip to content
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

Wrong type for property in dataclass if there exists an InitVar of the same name #4113

Closed
xIceArcher opened this issue Oct 31, 2022 · 3 comments
Labels
as designed Not a bug, working as intended

Comments

@xIceArcher
Copy link

Describe the bug
From https://docs.python.org/3/library/dataclasses.html:

If a field is an InitVar, it is considered a pseudo-field called an init-only field. As it is not a true field, it is not returned by the module-level fields() function. Init-only fields are added as parameters to the generated init() method, and are passed to the optional post_init() method. They are not otherwise used by dataclasses.

When a dataclass has both an InitVar and @property with the same name, outside of __init__() and __post_init__(), the variable should have the type as described by the @property annotation, since InitVars are not used outside of __init__() and __post_init__().

To Reproduce
Simply create a Python file with the following code

Expected behavior
The type of the property outside of __init__() and __post_init__() should be the type of the property, or at the very least, referencing these variables should not be treated as an error

Screenshots or Code

from dataclasses import InitVar, dataclass, field

@dataclass
class Foo:
    _bar: list[str] = field(init=False)
    bar: InitVar[str] = ''

    def __post_init__(self, bar):
        if type(bar) is property:
            self._bar = ['default', 'bar']
        else:
            self._bar = bar.split(',')

    @property
    def bar(self) -> str:
        return ','.join(self._bar)

f1 = Foo()
print(f'f1.bar is {f1.bar}') # f1.bar should be str

f2 = Foo(bar='non,default,bar')
print(f'f2.bar is {f2.bar}') # f2.bar should be str
No configuration file found.
No pyproject.toml file found.
stubPath /Users/xxx/Desktop/typings is not a valid directory.
Assuming Python platform Darwin
Searching for source files
Found 1 source file
pyright 1.1.277
/Users/xxx/Desktop/test.py
  /Users/xxx/Desktop/test.py:6:25 - error: Expression of type "Literal['']" cannot be assigned to declared type "property"
    "Literal['']" is incompatible with "property" (reportGeneralTypeIssues)
  /Users/xxx/Desktop/test.py:19:23 - error: Cannot access member "bar" for type "Foo"
    Member "bar" is an init-only field (reportGeneralTypeIssues)
  /Users/xxx/Desktop/test.py:22:23 - error: Cannot access member "bar" for type "Foo"
    Member "bar" is an init-only field (reportGeneralTypeIssues)
  /Users/xxx/Desktop/test.py:6:5 - error: Declaration "bar" is obscured by a declaration of the same name (reportGeneralTypeIssues)3 errors, 0 warnings, 0 informations 
Completed in 0.617sec

The errors in line 6 are valid (variable of same name with different types), but lines 19 and 22 should not be an error.

VS Code extension or command-line
Command line, version 1.1.277

@erictraut
Copy link
Collaborator

You are declaring the symbol Foo.bar twice here with two different types. You'll need to choose a different name for one of them to avoid the namespace conflict.

from dataclasses import InitVar, dataclass, field

@dataclass
class Foo:
    _bar: list[str] = field(init=False)
    baz: InitVar[str] = ''

    def __post_init__(self, baz):
        if type(baz) is property:
            self._bar = ['default', 'bar']
        else:
            self._bar = baz.split(',')

    @property
    def bar(self) -> str:
        return ','.join(self._bar)

f1 = Foo()
print(f'f1.bar is {f1.bar}')

f2 = Foo(baz='non,default,bar')
print(f'f2.bar is {f2.bar}')

@erictraut erictraut added the as designed Not a bug, working as intended label Oct 31, 2022
@xIceArcher
Copy link
Author

You are declaring the symbol Foo.bar twice here with two different types. You'll need to choose a different name for one of them to avoid the namespace conflict.

Yes, I agree that this namespace conflict should result in errors, specifically those in line 6.

However, I'd argue that referring to the variable outside of __init__() and __post_init__() should have an unambiguous type, based on the following logic:

  • InitVars are only used in __init__() and __post_init__(), and using InitVars outside __init__() and __post_init__() is an error
  • Therefore it follows that outside __init__() and __post_init__(), the "shadowed name" can only refer to the property, and not the InitVar

In other words, the following code fragment

from dataclasses import InitVar, dataclass, field

@dataclass
class Foo:
    _bar: list[str] = field(init=False)
    bar: InitVar[str] = '' # type: ignore

    def __post_init__(self, bar):
        if type(bar) is property:
            self._bar = ['default', 'bar']
        else:
            self._bar = bar.split(',')

    @property
    def bar(self) -> str:
        return ','.join(self._bar)

f1 = Foo()
print(f'f1.bar is {f1.bar}') # f1.bar should be str

f2 = Foo(bar='non,default,bar')
print(f'f2.bar is {f2.bar}') # f2.bar should be str

should not report any errors as f1.bar and f2.bar can only refer to the property bar and should have type str, since the InitVar bar is not valid outside of __init__() and __post_init__().

I believe this is important as it allows dataclass to be used to generate an __init__() method that has an argument with the same name as the property, like in this example:

class Foo:
    def __init__(self, bar: str = 'default,bar'):
        self._bar = bar.split(',')
    
    @property
    def bar(self) -> str:
        return ','.join(self._bar)

This way, there will be a consistent naming in both the __init__() method and property getter, since the function signature(s) of each will be shown as:

(class) Foo(bar: str = '')
(property) bar: str 

instead of as:

(class) Foo(baz: str = '')
(property) bar: str

as per your suggestion.

@erictraut
Copy link
Collaborator

You've added a # type: ignore comment in the code sample above. That suppresses the error but doesn't address it. You need to eliminate the namespace conflict if you want it to work correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
as designed Not a bug, working as intended
Projects
None yet
Development

No branches or pull requests

2 participants