2

I have an interface

class Moveable(Protocol):
    position: Tuple[int, int]

I implement the interface with a class that happens to use a getter to imitate the attribute position: Tuple[int, int]

class Player:
    _x: int
    _y: int

    @property
    def position(self) -> Tuple[int, int]:
        return (self._x, self._y)

I instantiate a Moveable variable to Player(), but Pylance raises an error

entity: Moveable = Player()
(class) Player()
Expression of type "Player" cannot be assigned to declared type "Moveable"
  "Player" is incompatible with protocol "Moveable"
    "position" is invariant because it is mutable
    "position" is an incompatible type
      "property" is incompatible with "Tuple[int, int]" PylancereportGeneralTypeIssues

The error goes away if I change the type annotation on the interface to property

class Moveable(Protocol):
    position: property # fixes Pylance alert

This isn't tenable as I may have classes that do not use a getter to produce a position but instead have it directly as an attribute. I want my interface to be agnostic as to whether classes implement a position: Tuple[int, int] member using a @property or as a direct attribute.

A non-ideal attempt at a solution was to use a Union, but that oddly isn't acceptable to Pylance either

class Moveable(Protocol):
    position: Union[Tuple[int, int], property]
class) Player()
Expression of type "Player" cannot be assigned to declared type "Moveable"
  "Player" is incompatible with protocol "Moveable"
    "position" is invariant because it is mutable
    "position" is an incompatible type
      Type "property" cannot be assigned to type "Tuple[int, int] | property"
        "property" is incompatible with "Tuple[int, int]" PylancereportGeneralTypeIssues

Seems like this should be supported. Is it? Anyway to achieve the effect?

Michael Moreno
  • 947
  • 1
  • 7
  • 24

1 Answers1

0

The solution is to use @property in both the protocol and the conforming class of protocol (at least according to the latest mypy, be aware that the different type checkers may behave differently). Here's the complete example based on the question:

from typing import Protocol, Tuple


class Movable(Protocol):
    @property
    def position(self) -> Tuple[int, int]:
        ...


class Player:
    _x: int
    _y: int

    def __init__(self, x: int = 0, y: int = 0):
        self._x = x
        self._y = y

    @property
    def position(self) -> Tuple[int, int]:
        return (self._x, self._y)


def print_position(obj: Movable) -> None:
    print(obj.position)


p = Player(x=1, y=2)
print_position(p)
Mik
  • 4,117
  • 1
  • 25
  • 32