A common way to generate a Python REST API client from an OpenAPI/Swagger specification is with OpenAPI Generator.
1. Obtain the OpenAPI spec
The API needs to expose a spec file, usually at one of these URLs:
https://api.example.com/openapi.json
https://api.example.com/swagger.json
https://api.example.com/openapi.yaml
Download it if desired:
curl -o openapi.json https://api.example.com/openapi.json
2. Generate with OpenAPI Generator
Install the generator
Using npm:
npm install -g @openapitools/openapi-generator-cli
Or run it through Docker, which avoids a local Java/npm setup:
docker run --rm \
-v "${PWD}:/local" \
openapitools/openapi-generator-cli generate \
-i /local/openapi.json \
-g python \
-o /local/generated-client
Generate a Python client locally
openapi-generator-cli generate \
-i openapi.json \
-g python \
-o generated-client \
--additional-properties=packageName=legacy_api_client,projectName=legacy-api-client
This typically creates a package structure such as:
generated-client/
legacy_api_client/
api/
models/
api_client.py
configuration.py
test/
pyproject.toml or setup.py
Install it into your Django project:
pip install -e ./generated-client
3. Use the generated client
Suppose the OpenAPI document defines a CustomersApi and an endpoint like GET /customers/{id}.
from legacy_api_client import ApiClient, Configuration
from legacy_api_client.api.customers_api import CustomersApi
configuration = Configuration(
host="https://legacy-api.example.com"
)
configuration.api_key["Authorization"] = "Bearer my-access-token"
with ApiClient(configuration) as api_client:
customers_api = CustomersApi(api_client)
customer = customers_api.get_customer(customer_id="cust_123")
print(customer.name)
Generated method names depend on the spec's operationId. For example, this endpoint:
paths:
/customers/{customer_id}:
get:
operationId: getCustomer
usually becomes something close to:
customers_api.get_customer(customer_id="cust_123")
4. Prefer a service-layer wrapper in Django
I would generally avoid calling generated client code directly from views, management commands, or migration orchestration logic. Wrap it so that generated-code details remain isolated.
# integrations/legacy_system/client.py
from legacy_api_client import ApiClient, Configuration
from legacy_api_client.api.assets_api import AssetsApi
class LegacySystemClient:
def __init__(self, base_url: str, access_token: str):
configuration = Configuration(host=base_url)
configuration.api_key["Authorization"] = f"Bearer {access_token}"
self.api_client = ApiClient(configuration)
self.assets_api = AssetsApi(self.api_client)
def get_asset(self, external_id: str):
return self.assets_api.get_asset(asset_id=external_id)
def create_asset(self, payload):
return self.assets_api.create_asset(asset_create_request=payload)
Then a Django management command or migration service can depend on your wrapper:
# migrations/services/asset_import.py
from integrations.legacy_system.client import LegacySystemClient
class AssetImportService:
def __init__(self, legacy_client: LegacySystemClient):
self.legacy_client = legacy_client
def import_asset(self, external_id: str):
source_asset = self.legacy_client.get_asset(external_id)
# Map the source API model to the local Django model.
# Validate, persist, and record migration status here.
return source_asset
That design is useful because an API spec regeneration may change generated method names or model structure, while the rest of your application only relies on your stable wrapper.
5. A practical migration-oriented generation command
For a checked-in generated client:
openapi-generator-cli generate \
-i https://legacy-api.example.com/openapi.json \
-g python \
-o clients/legacy_api \
--additional-properties=packageName=legacy_api_client,projectName=legacy-api-client,packageVersion=0.1.0 \
--global-property=apiDocs=false,modelDocs=false,apiTests=false,modelTests=false
In a real team, I’d put this in a Makefile or script:
generate-legacy-client:
openapi-generator-cli generate \
-i specs/legacy-api.openapi.yaml \
-g python \
-o clients/legacy_api \
--additional-properties=packageName=legacy_api_client,projectName=legacy-api-client \
--global-property=apiDocs=false,modelDocs=false,apiTests=false,modelTests=false
Then use CI to verify the generated client is current:
make generate-legacy-client
git diff --exit-code
This prevents a spec change from silently leaving the committed client out of date.
6. Alternative: generate a modern httpx client with openapi-python-client
Another popular option is openapi-python-client, which generates clients based on httpx.
pip install openapi-python-client
openapi-python-client generate --path openapi.json --output-path generated-client
Typical usage:
from legacy_api_client import Client
from legacy_api_client.api.customers import get_customer
client = Client(
base_url="https://legacy-api.example.com",
headers={"Authorization": "Bearer my-access-token"},
)
response = get_customer.sync_detailed(
client=client,
customer_id="cust_123",
)
if response.status_code == 200:
customer = response.parsed
For a Django migration application, either generator is valid. The main things I’d emphasize in an interview are:
- Treat the generated client as an integration boundary, not business logic.
- Pin the generator version for reproducible output.
- Version and validate the OpenAPI spec.
- Regenerate intentionally, ideally with a CI drift check.
- Add retries, timeouts, error normalization, and observability in your wrapper rather than editing generated files.
- Ensure migration jobs are idempotent: API retries must not accidentally create duplicate records.