I have a python class defining an interface
class InterfaceFoo:
pass
some abstract class
class AbstractBar:
pass
and perhaps a concrete class
class Bar(InterfaceFoo):
pass
implementing my interface (or, if you wish, both the interfaces Interface Foo
and AbstractBar
).
Now in some situations, I would like to have a type hint saying »I expect an instance of a class that is derived from AbstractBar
and implements InterfaceFoo
«. How can I do this in Python? Of course, I could go through my code and add all possible concrete classes that are subclasses of AbstractBar
and implement InterfaceFoo
but I think that's a very ugly solution.
def func(obj: AbstractBar implementing InterfaceFoo):
pass
This is possible in other languages, e.g. Objective-C, where you can write BaseClass<Interface>
. I'm wondering, if there is an analogue in Python to this?
The solution is indeed the typing.Protocol
. However the challenge here lies in the fact that you want a combination of nominal subtyping (i.e. a type declared to inherit from another) and structural subtyping (i.e. a type has a certain interface).
The Protocol
is defined in a way that prohibits simply using multiple inheritance here, because, as any type checker will be quick to tell you, "all bases of a protocol must be protocols". So we cannot simply inherit from any AbstractBar
and a Protocol
. Details about this are outlined in this section of PEP 544.
What we can do however is declare our abstract base class to be a protocol. Since it is abstract and thus not supposed to be instantiated directly, just like a protocol, this should work in most cases. Specifically, with the abc
module from the standard library, we have the option of specifying an abstract base class without inheriting from abc.ABC
by instead setting the metaclass to be abc.ABCMeta
.
Here is how that might look:
from abc import ABCMeta, abstractmethod
from typing import Protocol
class AbstractBase(Protocol, metaclass=ABCMeta):
@abstractmethod
def foo(self) -> int:
...
class SomeInterface(Protocol):
def bar(self) -> str:
...
class ABSomeInterface(AbstractBase, SomeInterface, Protocol):
pass
We can now define a function that expects its argument to be of type ABSomeInterface
like so:
def func(obj: ABSomeInterface) -> None:
print(obj.foo(), obj.bar())
Now if we want to implement a concrete subclass of AbstractBase
and we want that class to be compliant with our SomeInterface
protocol, we need it to also implement a bar
method:
class Concrete(AbstractBase):
def foo(self) -> int:
return 2
def bar(self) -> str:
return "x"
Now we can safely pass instances of Concrete
to func
and type checkers are happy:
func(Concrete())
Conversely, if we had another subclass of AbstractBase
that did not implement bar
(and thus didn't follow our SomeInterface
protocol), we would get an error:
class Other(AbstractBase):
def foo(self) -> int:
return 3
func(Other())
mypy
will complain like this:
error: Argument 1 to "func" has incompatible type "Other"; expected "ABSomeInterface" [arg-type]
note: "Other" is missing following "ABSomeInterface" protocol member:
note: bar
This is what we would expect and what we want. The abc
functionality is also preserved by using the ABCMeta
metaclass; so attempting to subclass AbstractBase
without implementing foo
would cause a runtime error upon reading that module.
Just for the sake of completeness, here is a full working example, where SomeInterface
is a generic protocol, to demonstrate that this also works as expected:
from abc import ABCMeta, abstractmethod
from typing import Protocol, TypeVar
T = TypeVar("T")
class AbstractBase(Protocol, metaclass=ABCMeta):
@abstractmethod
def foo(self) -> int:
...
class SomeInterface(Protocol[T]):
def bar(self, items: list[T]) -> T:
...
class ABSomeInterface(AbstractBase, SomeInterface[T], Protocol):
pass
def func(obj: ABSomeInterface[str], strings: list[str]) -> None:
n = obj.foo()
s = obj.bar(strings)
print(n * s.upper())
class Concrete(AbstractBase):
def foo(self) -> int:
return 2
def bar(self, items: list[T]) -> T:
return items[0]
if __name__ == "__main__":
func(Concrete(), ["a", "b", "c"])
It is important to note that there is no way around defining a "pure" SomeInterface
protocol. We cannot simply have a SomeInterface
class that we can both instantiate and use it as a protocol. That is in the nature of these things.
Intersection types as such do not (yet) exist in Python as far as I know, so this structural approach is the best we can do.