A little background, I essentially need to define an int
wrapper type, say MyInt
(among some other classes), and another generic Interval
type which can accept MyInt
objects as well as other types of objects. Since the types acceptable by the Interval
do not fall into a neat hierarchy, I thought this would be a perfect use-case for the experimental Protocol
, which in my case would require a couple of methods and a couple of @classmethod
s. All the methods return a "self-type", i.e., MyInt.my_method
returns a MyInt
. Here is a MCVE:
from dataclasses import dataclass
from typing import Union, ClassVar, TypeVar, Generic, Type
from typing_extensions import Protocol
_P = TypeVar('_P', bound='PType')
class PType(Protocol):
@classmethod
def maximum_type_value(cls: Type[_P]) -> _P:
...
@classmethod
def minimum_type_value(cls: Type[_P]) -> _P:
...
def predecessor(self: _P) -> _P:
...
def successor(self: _P) -> _P:
...
@dataclass
class MyInteger:
value: int
_MAX: ClassVar[int] = 42
_MIN: ClassVar[int] = -42
def __post_init__(self) -> None:
if not (self._MIN <= self.value <= self._MAX):
msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
raise ValueError(msg)
@classmethod
def maximum_type_value(cls) -> MyInteger:
return MyInteger(cls._MAX)
@classmethod
def minimum_type_value(cls) -> MyInteger:
return MyInteger(cls._MIN)
def predecessor(self) -> MyInteger:
return MyInteger(self.value - 1)
def successor(self) -> MyInteger:
return MyInteger(self.value + 1)
@dataclass
class Interval(Generic[_P]):
low: _P
high: _P
interval = Interval(MyInteger(1), MyInteger(2))
def foo(x: PType) -> PType:
return x
foo(MyInteger(42))
However, mypy complains:
(py37) Juans-MacBook-Pro: juan$ mypy mcve.py
mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note: Expected:
mcve.py:49: note: def maximum_type_value(cls) -> <nothing>
mcve.py:49: note: Got:
mcve.py:49: note: def maximum_type_value(cls) -> MyInteger
mcve.py:49: note: Expected:
mcve.py:49: note: def minimum_type_value(cls) -> <nothing>
mcve.py:49: note: Got:
mcve.py:49: note: def minimum_type_value(cls) -> MyInteger
Which to me is hard to understand. Why is return-type expecting <nothing>
? I tried simply not annotating cls
in the protocol:
_P = TypeVar('_P', bound='PType')
class PType(Protocol):
@classmethod
def maximum_type_value(cls) -> _P:
...
@classmethod
def minimum_type_value(cls) -> _P:
...
def predecessor(self: _P) -> _P:
...
def successor(self: _P) -> _P:
...
However, mypy complains with a similar error message:
mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note: Expected:
mcve.py:49: note: def [_P <: PType] maximum_type_value(cls) -> _P
mcve.py:49: note: Got:
mcve.py:49: note: def maximum_type_value(cls) -> MyInteger
mcve.py:49: note: Expected:
mcve.py:49: note: def [_P <: PType] minimum_type_value(cls) -> _P
mcve.py:49: note: Got:
mcve.py:49: note: def minimum_type_value(cls) -> MyInteger
Which to me, makes even less sense. Note, if I make these instance methods:
_P = TypeVar('_P', bound='PType')
class PType(Protocol):
def maximum_type_value(self: _P) -> _P:
...
def minimum_type_value(self: _P) -> _P:
...
def predecessor(self: _P) -> _P:
...
def successor(self: _P) -> _P:
...
@dataclass
class MyInteger:
value: int
_MAX: ClassVar[int] = 42
_MIN: ClassVar[int] = -42
def __post_init__(self) -> None:
if not (self._MIN <= self.value <= self._MAX):
msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
raise ValueError(msg)
def maximum_type_value(self) -> MyInteger:
return MyInteger(self._MAX)
def minimum_type_value(self) -> MyInteger:
return MyInteger(self._MIN)
def predecessor(self) -> MyInteger:
return MyInteger(self.value - 1)
def successor(self) -> MyInteger:
return MyInteger(self.value + 1)
Then, mypy
doesn't complain at all:
I've read about self-types in protocols in PEP 544, where it gives the following example:
C = TypeVar('C', bound='Copyable')
class Copyable(Protocol):
def copy(self: C) -> C:
class One:
def copy(self) -> 'One':
...
T = TypeVar('T', bound='Other')
class Other:
def copy(self: T) -> T:
...
c: Copyable
c = One() # OK
c = Other() # Also OK
Furthermore, in PEP484, regarding typing classmethods, we see this example:
T = TypeVar('T', bound='C')
class C:
@classmethod
def factory(cls: Type[T]) -> T:
# make a new instance of cls
class D(C): ...
d = D.factory() # type here should be D
What is wrong with my Protocol
/ class definition? Am I missing something obvious? I would appreciate any specific answers about why this is failing, or any work-around. But note, I need these attributes to be accessible on the class.
Note, I've tried using a ClassVar
, but that introduced other issues... namely, ClassVar
does not accept type-variables as far as I can tell ClassVar
's cannot be generic. And ideally, it would be a @classmethod
since I might have to rely on other meta-data I would want to shove in the class.
MyInteger
itself? I mean:def minimum_type_value(self) -> MyInteger:
? As you are using Python3.7, it should work. – Irwin