Interfaces
Last edited
Python doesn’t have an interface keyword like Go, instead it offers two ways to modeling interfaces:
- Inhertiance-based interfaces with abstract-base-classes (ABCs)
- 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 inheritancetyping.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