Demo code
#!/usr/bin/env python3
from abc import ABCMeta, abstractmethod
class Base(metaclass = ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, 'x')
)
@property
@abstractmethod
def x(self) -> float:
raise NotImplementedError
class Concrete:
x: float = 1.0
class Application:
def __init__(self, obj: Base) -> None:
print(obj.x)
ob = Concrete()
app = Application(ob)
print(issubclass(Concrete, Base))
print(isinstance(Concrete, Base))
print(type(ob))
print(Concrete.__mro__)
python test_typing.py returns:
1.0
True
False
<class '__main__.Concrete'>
(<class '__main__.Concrete'>, <class 'object'>)
and mypy test_typing.py returns:
test_typing.py:30: error: Argument 1 to "Application" has incompatible type "Concrete"; expected "Base"
Found 1 error in 1 file (checked 1 source
But if i change the line class Concrete: to class Concrete(Base):, i get for
python test_typing.py this:
1.0
True
False
<class '__main__.Concrete'>
(<class '__main__.Concrete'>, <class '__main__.Base'>, <class 'object'>)
and for mypy test_typing.py this:
Success: no issues found in 1 source file
If i add to my code this:
reveal_type(Concrete)
reveal_type(Base)
i get in both cases the same results for it from mypy test_typing.py:
test_typing.py:37: note: Revealed type is "def () -> vmc.test_typing.Concrete"
test_typing.py:38: note: Revealed type is "def () -> vmc.test_typing.Base"
Conclusion
Seems obvious, that MyPi have some problems with virtual base classes but non-virtual inheritance seems working as expected.
Question
How works MyPy's type estimation in these cases? Is there an workaround?
2nd Demo code
Using Protocol pattern:
#!/usr/bin/env python3
from abc import abstractmethod
from typing import Protocol, runtime_checkable
@runtime_checkable
class Base(Protocol):
@property
def x(self) -> float:
raise NotImplementedError
@abstractmethod
def __init__(self, x: float) -> None:
raise NotImplementedError
"""
@classmethod
def test(self) -> None:
pass
"""
class Concrete:
x: float = 1.0
class Application:
def __init__(self, obj: Base) -> None:
pass
ob = Concrete()
app = Application(ob)
Pros
- Working with
mypy:Success: no issues found in 1 source file - Working with
isinstance(Concrete, Base):True
Cons
- Not working with
issubclass(Concrete, Base):TypeError: Protocols with non-method members don't support issubclass() - Not checking the
__init__method signatures:__init__(self, x: float) -> Nonevs.__init__(self) -> None(Why returnsinspect.signature()the strings(self, *args, **kwargs)and(self, /, *args, **kwargs)here? Withclass Base:instead ofclass Base(Protocol):i get(self, x: float) -> Noneand(self, /, *args, **kwargs)) - ignoring the difference between
@abstractmethodand@classmethod(treats ANY method as abstract)
3rd Demo code
This time just an more complex example of the 1st code:
#!/usr/bin/env python3
from abc import ABCMeta, abstractmethod
from inspect import signature
class Base(metaclass = ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, 'x') and
(signature(subclass.__init__) == signature(cls.__init__))
)
@property
@abstractmethod
def x(self) -> float:
raise NotImplementedError
@abstractmethod
def __init__(self, x: float) -> None:
raise NotImplementedError
@classmethod
def test(self) -> None:
pass
class Concrete:
x: float = 1.0
def __init__(self, x: float) -> None:
pass
class Application:
def __init__(self, obj: Base) -> None:
pass
ob = Concrete(1.0)
app = Application(ob)
Pros
- Working with
issubclass(Concrete, Base):True - Working with
isinstance(Concrete, Base):False - Method signature check also for
__init__.
Cons
- Not working with MyPy:
test_typing.py:42: error: Argument 1 to "Application" has incompatible type "Concrete"; expected "Base" Found 1 error in 1 file (checked 1 source file)
4th Demo code
In some circumstances the following code might be an possible solution.
#!/usr/bin/env python3
from typing import Protocol, runtime_checkable
from dataclasses import dataclass
@runtime_checkable
class Rotation(Protocol):
@property
def x(self) -> float:
raise NotImplementedError
@property
def y(self) -> float:
raise NotImplementedError
@property
def z(self) -> float:
raise NotImplementedError
@property
def w(self) -> float:
raise NotImplementedError
@dataclass
class Quaternion:
x: float = 0.0
y: float = 0.0
z: float = 0.0
w: float = 1.0
def conjugate(self) -> 'Quaternion':
return type(self)(
x = -self.x,
y = -self.y,
z = -self.z,
w = self.w
)
class Application:
def __init__(self, rot: Rotation) -> None:
print(rot)
q = Quaternion(0.7, 0.0, 0.7, 0.0)
app = Application(q.conjugate())
Pros:
- Auto-generated
__init__method because of@dataclassusage. here:(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 1.0) -> None - Works with
isinstance():True - Works with mypy:
Success: no issues found in 1 source file
Cons:
- You need to hope, that the next developer uses
@dataclassalong with implementing your interface.. - Not usable for
__init__methods, that are not only taken class attributes.
Tipp: If an forced __init__ method is not required and only want to take care of the attributes, then just omit @dataclass.
5th Demo code
Updated the 4th code to provide more safety, but without implicit __init__ method:
#!/usr/bin/env python3
from abc import abstractmethod
from typing import Protocol, runtime_checkable
@runtime_checkable
class Rotation(Protocol):
@property
@abstractmethod
def x(self) -> float:
raise NotImplementedError
@property
@abstractmethod
def y(self) -> float:
raise NotImplementedError
@property
@abstractmethod
def z(self) -> float:
raise NotImplementedError
@property
@abstractmethod
def w(self) -> float:
raise NotImplementedError
class Quaternion:
_x: float = 0.0
_y: float = 0.0
_z: float = 0.0
_w: float = 1.0
@property
def x(self) -> float:
return self._x
@property
def y(self) -> float:
return self._y
@property
def z(self) -> float:
return self._z
@property
def w(self) -> float:
return self._w
def __init__(self, x: float, y: float, z: float, w: float) -> None:
self._x = float(x)
self._y = float(y)
self._z = float(z)
self._w = float(w)
def conjugate(self) -> 'Quaternion':
return type(self)(
x = -self.x,
y = -self.y,
z = -self.z,
w = self.w
)
def __str__(self) -> str:
return ", ".join(
(
str(self._x),
str(self._y),
str(self._z),
str(self._w)
)
)
def __repr__(self) -> str:
cls = self.__class__
module = cls.__module__
return f"{module + '.' if module != '__main__' else ''}{cls.__qualname__}({str(self)})"
class Application:
def __init__(self, rot: Rotation) -> None:
print(rot)
q = Quaternion(0.7, 0.0, 0.7, 0.0)
app = Application(q.conjugate())
Current conclusion
The Protocol way is unstable.
But the Metaclass way is not checkable, because it's not working with MyPy (because it's not static).
Updated question
Are there any alternative solutions to achieve some type of Interfaces (without class Concrete(Base)) AND make it type-safe (checkable)?