I have two base classes, Foo
and Bar
, and a Worker
class which expects objects that behave like Foo
. Then I add another class that implements all relevant attributes and methods from Foo
but I didn't manage to communicate this successfully to static type checking via mypy. Here's a small example:
class MyMeta(type):
pass
class Bar(metaclass=MyMeta):
def bar(self):
pass
class Foo:
def __init__(self, x: int):
self.x = x
def foo(self):
pass
class Worker:
def __init__(self, obj: Foo):
self.x = obj.x
Here Worker
actually accepts any Foo
-ish object, i.e. objects that have an attribute x
and a method foo
. So if obj
walks like a Foo
and if it quacks like a Foo
then Worker
will be happy. Now the whole project uses type hints and so for the moment I indicate obj: Foo
. So far so good.
Now there's another class FooBar
, which subclasses Bar
and behaves like Foo
but it can't subclass Foo
because it exposes its attributes via properties (and so the __init__
parameters don't make sense):
class FooBar(Bar):
"""Objects of this type are bar and they are foo-ish."""
@property
def x(self) -> int:
return 0
def foo(self):
pass
At this point, doing Worker(FooBar())
obviously results in a type checker error:
error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Foo"
Using an abstract base class
In order to communicate the interface of Foo
-ish to the type checker I thought about creating an abstract base class for Foo
-ish types:
import abc
class Fooish(abc.ABC):
x : int
@abc.abstractmethod
def foo(self) -> int:
raise NotImplementedError
However I can't make FooBar
inherit from Fooish
because Bar
has its own metaclass and so this would cause a metaclass conflict. So I thought about using Fooish.register
on both Foo
and FooBar
but mypy doesn't agree:
@Fooish.register
class Foo:
...
@Fooish.register
class FooBar(Bar):
...
class Worker:
def __init__(self, obj: Fooish):
self.x = obj.x
Which produces the following errors:
error: Argument 1 to "Worker" has incompatible type "Foo"; expected "Fooish"
error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Fooish"
Using a "normal" class as an interface
The next option I considered is creating an interface without inheriting from abc.ABC
in form of a "normal" class and then have both Foo
and FooBar
inherit from it:
class Fooish:
x : int
def foo(self) -> int:
raise NotImplementedError
class Foo(Fooish):
...
class FooBar(Bar, Fooish):
...
class Worker:
def __init__(self, obj: Fooish):
self.x = obj.x
Now mypy doesn't complain about the argument type to Worker.__init__
but it complains about signature incompatibility of FooBar.x
(which is a property
) with Fooish.x
:
error: Signature of "x" incompatible with supertype "Fooish"
Also the Fooish
(abstract) base class is now instantiable and a valid argument to Worker(...)
though it doesn't make sense since it doesn't provide an attribute x
.
The question ...
Now I'm stuck at the question on how to communicate this interface to the type checker without using inheritance (due to metaclass conflict; even if it was possible, mypy would still complain about signature incompatibility of x
). Is there a way to do it?