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

Automatically infer class variables based on declaration #4389

Closed
FeldrinH opened this issue May 18, 2023 · 14 comments
Closed

Automatically infer class variables based on declaration #4389

FeldrinH opened this issue May 18, 2023 · 14 comments
Assignees
Labels
fixed in next version (main) A fix has been implemented and will appear in an upcoming version

Comments

@FeldrinH
Copy link

There seems to be a limitation with Pylance detecting if a variable declared in a class is a class variable or not.

In particular, the following code:

from typing import ClassVar, Protocol

class SimpleProtocol(Protocol):
    a: ClassVar[str]

class SimpleClass:
    a = "test"

val: SimpleProtocol = SimpleClass()

Gives the following error:
image
As far as I understand, SimpleClass.a is in fact a class variable (and using it as such does not produce any runtime errors),

Changing SimpleClass definition to

class SimpleClass:
    a: ClassVar = "test"

gets rid of the error.

It would be nice if Pylance could automatically infer that a is a class variable without the need for explicit type annotations.

Tested with Pylance language server 2023.5.30 (pyright e1b6074b).

@erictraut
Copy link
Contributor

erictraut commented May 18, 2023

Pyright, the static code analyzer upon which pylance is built, recognizes three types of class-scoped variables: 1) Pure class variables, 2) Regular class variables, and 3) Pure instance variables.

In your code sample, your protocol class has indicated that a needs to be a pure class variable. In your SimpleClass class definition, you've defined it as a regular class variable. That's why it's deemed incompatible during protocol matching.

As you noticed, you can eliminate the mismatch by specifying that SimpleClass.a is a pure class variable by adding an explicit ClassVar to the annotation. You can also eliminate the mismatch by changing SimpleProtocol.a to be a regular class variable (i.e. eliminate the ClassVar from the annotation).

@FeldrinH
Copy link
Author

I'm a little confused.

If I understand you correctly, then this declares SimpleProtocol.a to be a regular class variable:

class SimpleProtocol(Protocol):
    a: str

Yet, with this declaration the following gives an error:

from typing import Protocol, Type

class SimpleProtocol(Protocol):
    a: str

def black_box() -> Type[SimpleProtocol]:
    raise NotImplementedError

typ: Type[SimpleProtocol] = black_box()
print(typ.a)

image

And the following does not give an error:

from typing import Protocol

class SimpleProtocol(Protocol):
    a: str

class SimpleClass:
    def __init__(self):
        self.a = "test"

val: SimpleProtocol = SimpleClass()

So in what sense is SimpleProtocol.a a 'regular class variable'? In both these examples the type checking seems to behave as if a was a pure instance variable.

@erictraut
Copy link
Contributor

PEP 544 (which introduced Protocol classes) doesn't provide a way to distinguish between the three different types of class-level variables. It introduced ClassVar, which is clearly meant for "pure class variables". For other variables, it seems to indicate that they should be treated as pure instance variables.

To distinguish between protocol class variables and protocol instance variables, the special ClassVar annotation should be used as specified by PEP 526

However, now that I re-read that statement, I realize that it leaves some room for interpretation.

I just checked with mypy, another type checker that was the reference implementation for PEP 544, and it appears to use the following interpretation for protocols:

class SimpleProtocol(Protocol):
    a: ClassVar[str] # Pure class variable
    b: str # Regular class variable

    def __init__(self):
        self.c: str # Pure instance variable

Currently, pyright treats b as a pure instance variable within a protocol (consistent with my reading of the PEP), and that's what's leading to the error you're seeing.

It would be more consistent if protocol and non-protocol classes worked in a consistent manner, and if that doesn't violate PEP 544 (as it apparently does not), then I'm happy to make that change in pyright. That would also make pyright consistent with mypy in this regard.

Could someone on the pylance team please transfer this issue to the pyright repo?

@FeldrinH
Copy link
Author

FeldrinH commented May 18, 2023

FWIW the example in my original post does typecheck in Pycharm with no errors and no warnings.

EDIT: To avoid any possible confusion. This is the example that passes all Pycharm typechecks:

from typing import ClassVar, Protocol

class SimpleProtocol(Protocol):
    a: ClassVar[str]

class SimpleClass:
    a = "test"

val: SimpleProtocol = SimpleClass()

@FeldrinH
Copy link
Author

Also probably worth noting:
The mypy interpretation of class and instance variables seems to introduce a different issue.

The following code passes mypy --strict with no issues found, but raises an error at runtime when accessing typ.a:

from typing import Protocol, Type

class SimpleProtocol(Protocol):
    a: str

class SimpleClass:
    def __init__(self) -> None:
        self.a = "test"

def black_box() -> Type[SimpleProtocol]:
    return SimpleClass

typ: Type[SimpleProtocol] = black_box()
print(typ.a)

@erictraut
Copy link
Contributor

Yes, that's a general problem with "regular class variables" in Python. There's generally no way for a static type checker to know whether they have been initialized at the class or instance level (or both). Most other programming languages (for good reason) force you to declare whether a variable is a class or instance variable, and they don't permit it to be both.

@FeldrinH
Copy link
Author

Fair enough. The main question that I am still unclear on is this:

If Pyright was changed to consider the following code as valid and well typed, would it violate some PEP or lead to problematic soundness issues?

from typing import ClassVar, Protocol

class SimpleProtocol(Protocol):
    a: ClassVar[str]

class SimpleClass:
    a = "test"

val: SimpleProtocol = SimpleClass()

@erictraut
Copy link
Contributor

If Pyright was changed to consider the following code as valid and well typed, would it violate some PEP or lead to problematic soundness issues?

I think this once again depends on how PEP 544 is interpreted. Looking to mypy (which, as I mentioned, was the reference implementation for PEP 544), it generates an error in this case. I'm struggling to reconcile mypy's behaviors. It seems to treat a = "test" as a "pure instance variable" in this case (which doesn't make sense because it's clearly being assigned as a class variable). It interprets it as a "regular class variable" in other cases.

Setting aside mypy's inconsistent behavior and thinking about this from first principles, I agree with your assertion that this should be sound from a typing perspective. SimpleClass.a should be interpreted as a "regular class variable", which satisfies the "pure class variable" defined in the protocol. (Note: the converse situation would not be type safe.)

My conclusion is that there are two changes that I should make in pyright:

  1. Remove the special-case handling for variables declared within the class body of a protocol class so they are categorized as "regular class variables" (as opposed to "pure instance variables") if they do not include a ClassVar annotation.
  2. When performing protocol type consistency checks, allow a "regular class variable" to satisfy a protocol that defines the symbol as a "pure class variable".

Does that sound right to you?

@FeldrinH
Copy link
Author

That does sound right to me. For the first change I don't really have a strong opinion either way. I fully agree with the second change and would be very happy if it was implemented.

erictraut pushed a commit to microsoft/pyright that referenced this issue May 20, 2023
…ble annotated as `ClassVar`. A class that defines this same variable as a "regular class variable" (one that is declared at the class level but can be overwritten on a per-instance basis) is now considered compatible with such a protocol. This addresses microsoft/pylance-release#4389.
@erictraut
Copy link
Contributor

I'm going to hold off on the first change and give it more thought.

I've implemented the second change, and this will be in next week's release of pyright (and the insider build of pylance).

@gramster
Copy link
Member

Did you decide about the first change Eric? Wondering if we can close this now.

erictraut pushed a commit to microsoft/pyright that referenced this issue Jun 20, 2023
…on. Previously, an error was reported when such variables were accessed from the class (as opposed to an instance of the class). Mypy (which was the reference implementation for PEP 544) does not report an error here. This addresses microsoft/pylance-release#4389.
@erictraut
Copy link
Contributor

Apologies, I forgot about this issue. After thinking about this more, I think we should go ahead and make the first change as well. This will be included in the next release of pyright — and the next insiders build of pylance. You can close this issue after this week's pylance is released.

@gramster
Copy link
Member

gramster commented Jun 20, 2023 via email

@heejaechang heejaechang added the fixed in next version (main) A fix has been implemented and will appear in an upcoming version label Jun 20, 2023
jbradaric added a commit to jbradaric/pyright-inlay-hints that referenced this issue Jun 20, 2023
commit 63f5658
Author: Erik De Bonte <[email protected]>
Date:   Tue Jun 20 10:50:18 2023 -0700

    Support PEP 712's new attribute assignment conversion (microsoft#5343)

commit 9cbe8c0
Author: Eric Traut <[email protected]>
Date:   Tue Jun 20 09:17:40 2023 -0700

    Fixed a bug that caused an incorrect false positive error for an `assert_type` call involving a `Callable[[], X]` type. Pyright was generating a signature with a positional-only separator in this case. This addresses python/typeshed#10325 (comment). (microsoft#5345)

    Co-authored-by: Eric Traut <[email protected]>

commit d12ad6e
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 23:59:08 2023 -0700

    Updated typeshed stubs to the latest version.

commit eff69f5
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 22:35:56 2023 -0700

    Fixed a bug that caused incorrect type inference for parameters in unannotated methods within child classes who derive from a generic parent class. This addresses a bug reported on stack overflow: https://stackoverflow.com/questions/76466874/trouble-specifying-type-parameter-when-inheriting-from-generic-class.

commit 43c5fae
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 20:17:00 2023 -0700

    Finished cleanup of test cases.

commit b0d4080
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 19:15:53 2023 -0700

    Next batch of test case cleanup.

commit efea9b9
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 18:26:49 2023 -0700

    Changed behavior of non-ClassVar variables within a protocol definition. Previously, an error was reported when such variables were accessed from the class (as opposed to an instance of the class). Mypy (which was the reference implementation for PEP 544) does not report an error here. This addresses microsoft/pylance-release#4389.

commit 5568b4d
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 18:00:31 2023 -0700

    Enhanced command-line version of pyright to allow file or directory names to be passed via stdin if `-` option is used in the command line. This addresses microsoft#5342.

commit cd257f7
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 15:38:13 2023 -0700

    Next batch of test case cleanup.

commit 0c5c7b6
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 14:23:50 2023 -0700

    Next batch of text case cleanup.

commit 899799d
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 13:33:41 2023 -0700

    Finished cleaning up "genericTypesX.py" test files.

commit 1111f1b
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 11:49:56 2023 -0700

    Started to untangle the hairball related to the "genericTypesX.py" test cases.

commit 67394ad
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 10:21:36 2023 -0700

    Continued cleanup of test cases.

commit 96e03aa
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 09:39:09 2023 -0700

    Did a cleanup pass for the dataclass test cases. Renamed and reordered test cases for maintainability.

commit b7df200
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 08:20:54 2023 -0700

    Fixed bug that resulted in a false positive error when defining a new type alias using the `TypeAliasType` constructor that defines no new type parameters but references an outer-scoped type parameter in the type alias definition. This addresses microsoft#5341.

commit d816d00
Author: Eric Traut <[email protected]>
Date:   Mon Jun 19 00:00:07 2023 -0700

    Started to do a pass over the test cases to make them more consistent.

commit 23bcbce
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 17:42:25 2023 -0700

    Fixed bug in code flow engine that led to incorrect type evaluation of a variable in a nested loop. This addresses https://github.com/microsoft/pylance-release/issues/4509.

commit 056415f
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 16:40:00 2023 -0700

    Fixed a bug in the control flow debugging code that prints the control flow graph. It was not correctly handling one of the node types which led to incomplete graphs.

commit 643bb1d
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 13:20:49 2023 -0700

    Improved type inference for lambdas in the case where a parameter includes a default value and the expected type doesn't include that parameter. This improvement was suggested in the [mypy issue tracker](python/mypy#15459). (microsoft#5337)

    Co-authored-by: Eric Traut <[email protected]>

commit 8ce23eb
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 12:55:22 2023 -0700

    Improved `reportUnnecessaryCast` so it works with types other than cl… (microsoft#5336)

    Improved `reportUnnecessaryCast` so it works with types other than class instances. This addresses microsoft#5333.

commit 52c8cac
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 12:41:24 2023 -0700

    Changed type printer (the component that renders types into text) to use the lowercase `type[x]` instead of `Type[x]`. It has now been four years since PEP 585 deprecated the use of the upper-case version, so most developers should be getting comfortable with the lowercase version at this point.

commit e3080b1
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 10:15:58 2023 -0700

    Fixed a bug that led to a false positive error under certain circumstances when a literal type argument was used in conjunction with a protocol that used a covariant type parameter and an implementation of that protocol that used an invariant type parameter. This addresses microsoft#5282. (microsoft#5332)

    Co-authored-by: Eric Traut <[email protected]>

commit 2b0f8d2
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 09:46:18 2023 -0700

    Improved hover text to display the calculated variance for a PEP 695-style class-scoped type variable when the user hovers over the type parameter in the type param list.

commit 02b7769
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 00:17:53 2023 -0700

    Fixed a false positive error arising from the use of a binary expression for a base class in a class declaration statement. This addresses microsoft#5326. (microsoft#5331)

    Co-authored-by: Eric Traut <[email protected]>

commit 2de35e3
Author: Eric Traut <[email protected]>
Date:   Sun Jun 18 00:00:06 2023 -0700

    Added documentation about higher-order functions.

commit 42a37f4
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 23:38:50 2023 -0700

    Re-enabled a test case that was previously disabled because it was broken.

commit 7ea11a1
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 23:17:32 2023 -0700

    Minor code cleanup — rename constant for clarity and refactor validation function. No functional change.

commit 53cb3f9
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 23:00:49 2023 -0700

    Improved consistency of parameter ordering internally to type evaluator. No functional change.

commit 9bf8231
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 22:48:18 2023 -0700

    Fixed a bug that led to incorrect type evaluation when passing a generic class (with a constructor that includes class-scoped TypeVars) as an argument for a callable parameter. The class was being specialized prematurely (with type arguments set to `Unknown`) before the constraint solver was able to solve the higher-order function's type variables. This addresses microsoft#5324. (microsoft#5328)

    Co-authored-by: Eric Traut <[email protected]>

commit 47cd514
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 22:21:52 2023 -0700

    Changed auto-variance algorithm to ignore `__new__` and `__init__` methods for purposes of calculating the variance of a TypeVar. This mirrors the behavior of mypy. (microsoft#5327)

    Co-authored-by: Eric Traut <[email protected]>

commit 3021b9c
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 21:52:57 2023 -0700

    Minor code cleanup — removed dead code and converted lambda to function. No functional change.

commit 327ce37
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 20:30:36 2023 -0700

    Added test case for microsoft#5027.

commit 0559382
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 14:19:12 2023 -0700

    Fixed false negative when a literal and non-literal are assigned to the same TypeVar in an invariant context. This addresses microsoft#5321. (microsoft#5323)

    Co-authored-by: Eric Traut <[email protected]>

commit 2829429
Author: Eric Traut <[email protected]>
Date:   Sat Jun 17 12:33:13 2023 -0700

    Fixed bug that led to an incorrect type evaluation for nested call expressions where an inner call expression used a ParamSpec. This addresses microsoft#5281. (microsoft#5322)

    Co-authored-by: Eric Traut <[email protected]>
@heejaechang
Copy link
Contributor

This issue has been fixed in prerelease version 2023.6.31, which we've just released. You can find the changelog here: CHANGELOG.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fixed in next version (main) A fix has been implemented and will appear in an upcoming version
Projects
None yet
Development

No branches or pull requests

4 participants