17

(Related, but not duplicated: How to annotate attribute that can be implemented as property?)

I want to create a Protocol, in which a field can be implemented by both a simple type and property. For example:

class P(Protocol):
    v: int


@dataclass
class Foo(P):
    v: int


class Bar(P):
    @property
    def v(self) -> int: # ERROR
        return

But the code above doesn't type check. How should I fix it?

Note: I want to solve this issue without rewriting Foo and Bar, because Foo and Bar are not what I implemented.

According to this issue the below code is not a solution because read-only property and a simple member have subtly different semantics.

class P(Protocol):
    @property
    def v(self) -> int: # declare as property
        ...

Pyright denies this Protocol due to the difference.

tamuhey
  • 2,904
  • 3
  • 21
  • 50

2 Answers2

12

In general, declare the Protocol using a read-only property, not a read/write field:

class P(Protocol):
    @property
    def v(self) -> int:
        pass

This is needed because a read-only protocol attribute is satisfied by both a read-only property and a read/write field. In contrast, a read/write protocol attribute is satisfied only by a read/write field, not a read-only property.


As PyRight insists that fields and properties are different kinds of attributes, the attribute must be declared with both variants – once as a field and once as an attribute. For simple protocols, this can be done by declaring a separate field and property variant of the property:

# field only
class Pf(Protocol):
    v: int

# property only
class Pp(Protocol):
    @property
    def v(self) -> int:
        return 1

# Either field or property
P = Union[Pf, Pp]

This is valid for both MyPy and PyRight.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • As I mentioned above, this is not a solution. Because read-only property and a simple field are slightly different. For example: https://github.com/python/mypy/issues/10797 – tamuhey Jul 12 '21 at 02:51
  • In short, `property` is a class attribute but a simple field is not a class attribute. – tamuhey Jul 12 '21 at 02:53
  • 1
    `mypy` accepts the code in this answer fine for me. If you ask me, taking `type` of a thing whose static type is a protocol is the problem, not the fact that the property is implemented as a field. – Silvio Mayolo Jul 12 '21 at 02:58
  • @Yohei You asked for static type checking. Runtime behavior and static behavior are not equivalent. Static type checkers use classes and especially protocols to define instance behavior - they are not primarily concerned with how classes behave. The runtime type of a protocol instance is statically undefined and can be and do whatever it wants. – MisterMiyagi Jul 12 '21 at 05:28
  • @MisterMiyagi Ok, I understand. But actually, Pyright denies this solution because there are subtle differences between a simple field and property (see [here](https://github.com/microsoft/pyright/issues/2072)). I thought the difference which the author of Pyright author mentioned is that code, but this may not be right, and I may have you confused. – tamuhey Jul 12 '21 at 08:17
  • 1
    @Yohei Since Python types are not static, static type checkers ultimately apply a simplified model of how things actually work. For many things there is no "right" or "wrong" to this, since by design these models are not accurate. LSP says that a read-only attribute cannot substitute for a read/write field, but whether one treats fields and properties as equivalent attributes depends on the model resolution and goals (I personally find PyRight's stance impractical). I can try to come up with something that works for both MyPy and PyRight, though. – MisterMiyagi Jul 12 '21 at 08:43
  • 1
    @Yohei I've added a variant that works for both MyPy and PyRight. – MisterMiyagi Jul 12 '21 at 09:20
1

If you are not strict about the type annotated for the property's getter, you could define P as:

from typing import Protocol, Union, ClassVar

class P(Protocol):
    v: Union[int, ClassVar]
truhanen
  • 563
  • 6
  • 9