.agents/skills/dignified-python/references/advanced/interfaces.md
Read when: Creating ABC/Protocol classes, writing @abstractmethod, designing gateway interfaces
ABCs (nominal typing) and Protocols (structural typing) serve different purposes. Choose based on ownership and coupling needs.
| Use Case | Recommended | Why |
|---|---|---|
| Internal interfaces you control | ABC | Explicit enforcement, runtime validation, code reuse |
| Third-party library boundaries | Protocol | No inheritance required, loose coupling |
| Plugin systems with isinstance checks | ABC | Reliable runtime type validation |
| Minimal interface contracts (1-2 methods) | Protocol | Less boilerplate, focused contracts |
Default for internal application code you own: ABC. Default for external library facades: Protocol.
# CORRECT: Use ABC for interfaces
from abc import ABC, abstractmethod
class Repository(ABC):
@abstractmethod
def save(self, entity: Entity) -> None:
"""Save entity to storage."""
...
@abstractmethod
def load(self, id: str) -> Entity:
"""Load entity by ID."""
...
class PostgresRepository(Repository):
def save(self, entity: Entity) -> None:
# Implementation
pass
def load(self, id: str) -> Entity:
# Implementation
pass
from abc import ABC, abstractmethod
from dataclasses import dataclass
# Define the interface
class DataStore(ABC):
@abstractmethod
def get(self, key: str) -> str | None:
"""Retrieve value by key."""
...
@abstractmethod
def set(self, key: str, value: str) -> None:
"""Store value with key."""
...
# Real implementation
class RedisStore(DataStore):
def get(self, key: str) -> str | None:
return self.client.get(key)
def set(self, key: str, value: str) -> None:
self.client.set(key, value)
# Fake for testing
class FakeStore(DataStore):
def __init__(self) -> None:
self._data: dict[str, str] = {}
def get(self, key: str) -> str | None:
if key not in self._data:
return None
return self._data[key]
def set(self, key: str, value: str) -> None:
self._data[key] = value
# Business logic accepts interface
@dataclass
class Service:
store: DataStore # Depends on abstraction
def process(self, item: str) -> None:
cached = self.store.get(item)
if cached is None:
result = expensive_computation(item)
self.store.set(item, result)
else:
result = cached
use_result(result)
Protocols excel at defining interfaces for code you don't control:
# CORRECT: Protocol for third-party library facade
from typing import Protocol
class HttpClient(Protocol):
"""Interface for HTTP operations - decouples from requests/httpx/aiohttp."""
def get(self, url: str) -> Response: ...
def post(self, url: str, data: dict) -> Response: ...
# Any HTTP library that has these methods works - no inheritance needed
def fetch_data(client: HttpClient, endpoint: str) -> dict:
response = client.get(endpoint)
return response.json()
Protocols are also appropriate for minimal, focused interfaces:
# CORRECT: Protocol for structural typing with minimal interface
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
def cleanup_resources(resources: list[Closeable]) -> None:
for r in resources:
r.close()
@runtime_checkable only checks method existence, not signaturesBefore defining an interface (ABC or Protocol):
Default for internal application code you own: ABC. Default for external library facades: Protocol.