Achieving interface without inheritance in Python

Question:

I have a sorted linked list

class SortedLinkedList:
    # ...
    def insert(self, value: int):
        # ...
        if node.value > value:
            self.add_before(node, value)
        # ...

I would like to generalize the type of values that a Node can hold from only ints to any object that overloads the > operator by implementing the __gt__() magic method.

In other languages I would achieve this by using an Interface, but Python apparently has no analog. I’ve seen suggestions to fake interfaces by using abstract classes like

class Sortable(ABC):
    @abstractmethod
    def __gt__(self, other) -> bool:
        pass

class SortedLinkedList:
    # ...
    def insert(self, value: Sortable, node: Node):
        # ...

The problem is this approach requires extending and using subclasses from Sortable, which means types that already have > functionality like integers cannot be used

linkedlist.insert(5) # Pylance red squiggles
Argument of type "Literal[5]" cannot be assigned to
parameter "value" of type "Sortable" in function "insert"
  "Literal[5]" is incompatible with "Sortable" Pylance(reportGeneralTypeIssues) 

I understand that Interfaces are not necessary pre-runtime given Python’s dynamic duck typing and implicit style. I am not a fan, and am opting to use available tooling like typing and Pylance to achieve a strictly typed developer experience.

I am also not looking to use runtime checks like .hasattr(value, '__gt__'). I’m wanting this to register on the type system/language server/IDE level, as expressivity, readability, and IDE intellisense are the main benefits of strict typing.

Is there any way to achieve this?

Asked By: Michael Moreno

||

Answers:

What you’re looking for is typing.Protocol.

class Sortable(Protocol):
    def __gt__(self, other) -> bool: ...

With this, any class that defines __gt__ will be detected as an implicit subtype of Sortable. Note that in Pythons >= 3.8, you may need to to make other positional:

class Sortable(Protocol):
    def __gt__(self, other, /) -> bool: ...

This is because type checkers might not handle dunder-methods like __gt__ any different from a normal method and thus expects that the arguments might need to be callable by keyword. Adding the / tells the checkers that __gt__‘s arguments won’t ever need to be passed by keyword. Conversely, if an argument will only be passed by keyword, mark the protocol’s method accordingly:

class Something(Protocol):
    def method(self, *, name) -> str: ...
Answered By: pydsigner
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.