Modern Python SDK Design: Using Generics and Inheritance for Clean APIs
Overview of Python SDK Architecture
Constructing a Software Development Kit (SDK) involves more than wrapping HTTP calls in functions. A well-designed SDK provides a Pythonic interface that hides the complexity of headers, JSON parsing, and status code handling from the end user. This guide explores a sophisticated architectural pattern that utilizes
Prerequisites and Toolkit
Before building, ensure you have a firm grasp of Python type hinting and object-oriented programming. You should be familiar with asynchronous concepts, though this tutorial focuses on synchronous implementations for clarity. The following tools are essential:
- Pydantic: For data modeling and automatic validation of API responses.
- HTTPX: A modern, feature-rich HTTP client for Python that serves as our network engine.
- TypeVar and Generic: Standard library components from the
typingmodule used to create reusable code that adapts to different resource types.
Building the Low-Level HTTP Client
The foundation of the SDK is a specialized client that manages authentication and base URL configurations. Instead of repeating authorization headers in every request, we centralize this logic. This client acts as a gateway, providing methods for standard HTTP verbs like GET, POST, PUT, and DELETE while ensuring all requests include the necessary bearer tokens.
import httpx
class APIHTTPClient:
def __init__(self, token: str, base_url: str):
self.client = httpx.Client(
base_url=base_url,
headers={"Authorization": f"Bearer {token}"}
)

def request(self, method: str, endpoint: str, **kwargs):
return self.client.request(method, endpoint, **kwargs)
def get(self, endpoint: str):
return self.request("GET", endpoint)
## Implementing the Base API Model with Generics
To avoid the "God Class" anti-pattern where a single client object contains hundreds of methods for every possible resource, we move resource-specific logic into the models themselves. By creating a `BaseAPIModel` that inherits from Pydantic's `BaseModel`, we can define standard CRUD operations once and apply them to any resource, such as Users, Invoices, or Products.
```python
from typing import TypeVar, Generic, Type, List
from pydantic import BaseModel
T = TypeVar("T", bound="BaseAPIModel")
class BaseAPIModel(BaseModel, Generic[T]):
id: int | None = None
resource_path: str = ""
@classmethod
def find(cls: Type[T], client: APIHTTPClient) -> List[T]:
response = client.get(cls.resource_path)
return [cls(**item) for item in response.json()]
def save(self, client: APIHTTPClient):
if self.id:
client.request("PUT", f"{self.resource_path}/{self.id}", json=self.model_dump())
else:
response = client.request("POST", self.resource_path, json=self.model_dump())
self.id = response.json().get("id")
Creating Specific Resource Models
With the base logic established, creating a new resource becomes trivial. You simply define the fields and the endpoint path. The model automatically gains full CRUD capabilities without any additional boilerplate code. This approach ensures that your SDK remains consistent across different data types, as every resource follows the same method signatures for loading and saving.
class User(BaseAPIModel["User"]):
resource_path = "users"
name: str
email: str
# Usage Example
client = APIHTTPClient(token="secret_key", base_url="https://api.example.com")
users = User.find(client)
new_user = User(name="Alice", email="[email protected]")
new_user.save(client)
Syntax Notes and Conventions
This design relies heavily on Self-Referential Generics. By passing the class itself into the Generic type, Python's type checkers (like Mypy) can correctly infer that User.find() returns a List[User] rather than a list of the base class. We also utilize Dependency Injection by passing the client instance to the model methods, which facilitates easier unit testing and mocking.
Tips and Common Gotchas
One frequent mistake is forgetting to set the resource_path on the subclass, which results in 404 errors during API calls. Additionally, be mindful of Tight Coupling. While inheritance reduces code, it binds your models closely to your HTTP client. If you plan to support both REST and GraphQL, you may need to abstract the communication layer further to maintain flexibility. For large-scale SDKs, consider implementing pagination within the find method to prevent memory issues when dealing with thousands of records.