2

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?

2
  • depending on why you're doing that I'd either use overloads or a Protocol Commented Nov 4, 2022 at 16:50
  • see peps.python.org/pep-0544/#rejected for why this might not be idiomatic in Python Commented Nov 4, 2022 at 17:02

2 Answers 2

6

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.

Sign up to request clarification or add additional context in comments.

2 Comments

That’s a great deal of extra keyboard use for the promise of static duck typing :-/
Turning ABCs into Protocol can break a lot of things. For example, issubclass(ABSomeInterface, AbstractBase) would raise a TypeError if they are Protocol.
1

https://stackoverflow.com/a/74320795/955091 is not ideal because turning ABCs into Protocol can break a lot of things. For example, issubclass(ABSomeInterface, AbstractBase) would raise a TypeError if they are Protocol.

The less breaking solution would be add if TYPE_CHECKING: for static type checking only, so that you can keep ABSomeInterface and AbstractBase non-protocol while avoiding TypeError: Protocols can only inherit from other protocols.

if TYPE_CHECKING:
    # During type checking, we define a class that inherits from both AbstractBar and InterfaceFoo
    class AbstractBarImplementingInterfaceFoo(AbstractBar, InterfaceFoo):
        pass
else:
    # At runtime, we define AbstractBarImplementingInterfaceFoo as a Protocol
    class AbstractBarImplementingInterfaceFoo(Protocol):
        pass

def func(obj: AbstractBarImplementingInterfaceFoo):
    pass

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.