Interfaces

Last edited

Python doesn’t have an interface keyword like Go, instead it offers two ways to modeling interfaces:

  1. Inhertiance-based interfaces with abstract-base-classes (ABCs)
  2. Structural subtyping interfaces with protocols

Interfaces with ABCs

Example 1: Working ABC with inherted method

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass
    @abstractmethod
    def move(self):
        pass
    def jump(self): # not ABC, but since inherited it can be called
        return f"{self.__class__.__name__} is jumping"

class Dog(Animal):
    def speak(self):
        return "Woof Woof"
    def move(self):
        return "Dog runs"

>>> from animals import Dog
>>> dog = Dog()
>>> dog.speak()
'Woof Woof'
>>> dog.move()
'Dog runs'
>>> dog.jump()
'Dog is jumping'

Example 2: FileReaderInterface

This example catches a missing method it required

from abc import ABC, abstractmethod

class FileReaderInterface(ABC):
    """Interface for file readers."""
    @abstractmethod
    def extract_text(self) -> str:
        """Return text extracted from the loaded file."""

class PdfReader(FileReaderInterface):
    """Extract text from a PDF."""
    def extract_text(self) -> str:
        """Return text extracted from the loaded PDF."""
        return "Extracted PDF text"

class EmailReader(FileReaderInterface):
    """Extract text from an Email."""
    def extract_email_text(self) -> str: # incorrectly named
        """Return text extracted from the loaded email."""
        return "Extracted email text"


>>> from readers_abc import PdfReader, EmailReader

>>> pdf_reader = PdfReader()
>>> email_reader = EmailReader()
# Traceback (most recent call last):
#   ...
# TypeError: Can't instantiate abstract class EmailReader
# without an implementation for abstract method 'extract_text'

Structural subtyping interfaces with protocols

Python 3.8 added typing.Protocol.

Protocols suggest the contract rather than enforce it. A static type checker can verify the contract before your code runs, but Python itself won’t stop the code from executing if that contract isn’t met.

Protocols are often the best fit when you don’t need interface enforcement at runtime, or when the classes involved don’t share a natural inheritance relationship.

Definition

Structual subtyping: If your object has the methods (structure) the protocol lists, you are that type (you’re a subtype) (duct typing).

Example 1: FileReaderProtocol

This is the parallel to FileReaderInterface.

from typing import Protocol

class FileReaderProtocol(Protocol):
    """Protocol for file readers."""
    def extract_text(self) -> str:
        """Return text extracted from the loaded file."""
        ...

The concrete class satifies the protocol by implementing the protocol’s method.

class PdfReader:
    """Extract text from a PDF."""
    def extract_text(self) -> str:
        """Return text extracted from the loaded PDF."""
        return "Extracted PDF text"

class EmailReader:
    """Extract text from an Email."""
    def extract_text(self) -> str:
        """Return text extracted from the loaded email."""
        return "Extracted email text"

Note there, is no inhertiance, but static type checkers will pick up on the protocol.

Now let’s use the new protocol and our two implementations.

def read(reader: FileReaderProtocol, path: str) -> str:
    reader.load_file(path) # note I skipped this method above for terseness
    return reader.extract_text()

# Static type checker will ensure the protocol is met
read(PdfReader(), "/reports/report.pdf")
read(EmailReader(), "/mail/message.eml")

Conclusion

Now that you’ve defined an interface through either of these methods, you use duck typing to call the methods, they interfaces are just to ensure you implement what the duck requires.

When to use each

  • abc.ABC - enforced at runtime, requires inheritance
  • typing.Protocol - checked statically, requires only matching shape

abc.ABC - when you’re in an inheritance context with control over the hierarchy and need the interface to have runtime enforcement at instantiation time.
typing.Protocol. - When you want static checking without inheritance

Resources

Python Interfaces (realpython)