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

Protocol incompatibility with Final class variable #17175

Open
DiegoBaldassarMilleuno opened this issue Apr 26, 2024 · 4 comments
Open

Protocol incompatibility with Final class variable #17175

DiegoBaldassarMilleuno opened this issue Apr 26, 2024 · 4 comments
Labels
bug mypy got something wrong

Comments

@DiegoBaldassarMilleuno
Copy link

Bug Report

Given a protocol with just a read-only property, mypy allows it to bind to a class with that property declared as a ClassVar, but not with it declared as Final.

To Reproduce

I also tested the following code in pyright, and it exhibits the same behavior.

from __future__ import annotations
import typing
import collections

class Proto(typing.Protocol):
    @property
    def _fields(self) -> tuple[str, ...]: ...

class Type1:
    _fields: typing.ClassVar[tuple[str, str]] = ('a', 'b')
class Type2:
    # From PEP 591: Type checkers should infer a final attribute that is initialized in a class body as being a class variable.
    _fields: typing.Final[tuple[str, str]] = ('a', 'b')
Type3 = collections.namedtuple('Type3', ['a', 'b'])
#       ^ _fields defined internally
class Type4(typing.NamedTuple):
    # _fields defined internally
    a: int
    b: float

def f1(_: Proto) -> None: ...
f1(Type1)  # OK
f1(Type2)  # Error
f1(Type3)  # Error
f1(Type4)  # Error

def f2(_: tuple[str, ...]) -> None: ...
f2(Type1._fields)  # These are all OK
f2(Type2._fields)  #
f2(Type3._fields)  #
f2(Type4._fields)  #

Expected Behavior

If I understand correctly, Proto should be able to bind to any object obj such that obj._fields is an expression of type tuple[str, ...].
Type1, ..., Type4 all satisfy this requirement, but only Type1 binds to Proto as expected.

Actual Behavior

/tmp/delme2.py:23: error: Argument 1 to "f1" has incompatible type "type[Type2]"; expected "Proto"  [arg-type]
/tmp/delme2.py:23: note: Only class variables allowed for class object access on protocols, _fields is an instance variable of "Type2"
/tmp/delme2.py:24: error: Argument 1 to "f1" has incompatible type "type[Type3]"; expected "Proto"  [arg-type]
/tmp/delme2.py:24: note: Only class variables allowed for class object access on protocols, _fields is an instance variable of "Type3"
/tmp/delme2.py:25: error: Argument 1 to "f1" has incompatible type "type[Type4]"; expected "Proto"  [arg-type]
/tmp/delme2.py:25: note: Only class variables allowed for class object access on protocols, _fields is an instance variable of "Type4"
Found 3 errors in 1 file (checked 1 source file)

Mypy says that _fields is an instance variable for Type2, Type3 and Type4, but just below I can access it from the class just fine and no error is reported.
pyright gives somewhat similar results, so I wonder if I'm missing something.

Your Environment

  • Mypy version used: mypy 1.10.0 (compiled: yes)
  • Mypy command-line flags: --strict
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.12.3
@DiegoBaldassarMilleuno DiegoBaldassarMilleuno added the bug mypy got something wrong label Apr 26, 2024
@erictraut
Copy link

I think mypy is correct here. Final means that a value is both read-only and immutable. Your protocol definition here does not guarantee immutability of the value of _fields, so Type2, Type3 and Type4 are not compatible with the protocol.

Pyright also generates the same errors as mypy in this case.

A ReadOnly special form was recently added to the type system as part of PEP 705. It currently works only within a TypedDict definition, but there has been some discussion of supporting it in protocols and other classes. If this extension were made to the type system, it would allow you to do what you're trying to do here.

@DiegoBaldassarMilleuno
Copy link
Author

Thank you for the answer. So it seems that it's not currently possible.
However, I find it weird that mypy reports that

Only class variables allowed [...], _fields is an instance variable of "TypeX"

when it is not, it's a class variable. Isn't that a bug?

@erictraut
Copy link

The protocol defines _fields as a property, which means it should be interpreted as a read-only instance variable. That's how the typing spec indicates properties should be interpreted when they're used in protocols. Mypy is trying to tell you that if you access _fields from the class object Type2, you will get a tuple[str, str] object, but if you try to access _fields from a class object of type Proto2, you will get a property object.

So I understand why you would find the error message somewhat confusing, but it's consistent with the way protocols are defined to work.

@DiegoBaldassarMilleuno
Copy link
Author

I see, thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

2 participants