Interface patterns in Python
Overview
In Python, an interface is a contract that defines the methods a class must implement, without specifying how they work. Since Python lacks an `interface` keyword, we build interfaces using one of three patterns: - Informal interfaces using duck typing, where we rely on methods existing rather than checking types. - Formal interfaces using Abstract Base Classes (`abc`), which enforce implementation at runtime. - Structural subtyping using `typing.Protocol`, which allows static type checkers to verify contracts without inheritance. Think of an interface like a USB port: the port defines the shape and the pins (the interface), and any device (mouse, keyboard, drive) that fits that shape can be plugged in and used by the computer, regardless of its internal circuitry. A common trap is creating massive, monolithic interfaces that force implementers to write dummy methods. This creates a brittle architecture where classes are coupled to methods they do not use. from typing import Protocol class CloudProvider(Protocol): def save_file(self, path: str) -> None: ... def send_sms(self, number: str) -> None: ... class StorageOnlyProvider: def save_file(self, path: str) -> None: pass def send_sms(self, number: str) -> None: raise NotImplementedError("Storage only") This "fat interface" trap forces classes to implement methods just to raise `NotImplementedError`, which is covered fully in `isp`.
Interfaces decouple the code that uses an object from the code that implements it. This allows you to swap dependencies, like databases or payment gateways, without changing the core business logic.
Where used: Dependency Injection, Plugin Systems, Mocking in Tests
Why learn this
- Write code that depends on abstractions rather than concrete classes, making it easier to test.
- Implement `SOLID` principles effectively, particularly Dependency Inversion and Open/Closed.
- Create pluggable architectures where new features can be added without modifying existing code.
Code walkthrough
from typing import Protocol
class Logger(Protocol):
def log(self, msg: str) -> None:
...
class ConsoleLogger:
def log(self, msg: str) -> None:
print(f'CONSOLE: {msg}')
def process_data(logger: Logger):
logger.log('Data processed')
logger = ConsoleLogger()
process_data(logger)
Focus: def process_data(logger: Logger):
Aha moment
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def pay(self, amount: int):
pass
class StripeGateway(PaymentGateway):
pass
gateway = StripeGateway()
Prediction: What happens when we instantiate `StripeGateway`?
Common guess: It creates a `StripeGateway` object.
It raises a `TypeError` indicating "Can't instantiate abstract class `StripeGateway` with abstract methods `pay`". An abstract base class strictly enforces its contract, so subclasses cannot be instantiated unless all abstract methods are implemented.
Common mistakes
- Violating the contract: Implementing an interface but changing the method signature, such as accepting different arguments or returning a different type. This breaks the code that relies on the interface.
- Fat interfaces: Putting too many unrelated methods into a single interface. Classes are forced to implement methods they do not need, often by raising `NotImplementedError`. The fix is to split the interface into smaller, focused ones based on the Interface Segregation Principle.
Glossary
- Abstract Base Classes
- Special classes in Python that cannot be used directly, but act as templates forcing other classes to implement specific methods. For example, using `from abc import ABC, abstractmethod`, any class inheriting from `ABC` with `@abstractmethod` methods must implement them before it can be instantiated.
- SOLID principles
- A set of five core design guidelines in software engineering intended to make programs easier to understand, maintain, and extend. For example, the `S` in `SOLID` stands for Single Responsibility, meaning a class should do one thing only.
Recall questions
- What is the primary purpose of an interface in Python?
- Why are interfaces useful when dealing with dependencies like databases or external APIs?
- What are the three main patterns used to implement interfaces in Python?
- What is a major downside of creating a single, massive interface?
Understanding checks
What happens when this code runs?
It raises a `TypeError`.
`Dog` inherits from an Abstract Base Class but fails to implement the abstract method `speak()`. Python enforces this contract at instantiation time.
Why would you use `typing.Protocol` instead of `abc.ABC` for an interface?
With `Protocol`, classes do not need to explicitly inherit from it because of structural subtyping. They only need to implement the required methods.
This is extremely useful when you want to define an interface for classes you don't control, or when you want loose coupling without the strictness of explicit inheritance.
Practice tasks
Enforce the Interface
The provided code defines a basic `Notifier` interface but does not strictly enforce it. Modify `Notifier` to use the `abc` module so that subclasses are forced to implement the `send` method before they can be instantiated.
Challenge
A Pluggable Data Exporter
Using `typing.Protocol`, define an interface `Exporter` with an `export(data: dict) -> str` method. Implement a `JSONExporter` that returns the data as a JSON string and a `CSVExporter` that returns the data as comma-separated values (keys on the first line, values on the second). Finally, write a function `export_data(exporter, data)` that takes an `Exporter` and returns the exported string.
Continue learning
Previous: ABC Module and Abstract Methods
Previous: Structural Subtyping with Protocols
Previous: Duck Typing
Next: Single Responsibility Principle
Next: Open/Closed Principle
Next: Liskov Substitution Principle
Next: Interface Segregation Principle