Engineering

Generate a Python SDK from OpenAPI: end-to-end in 2026

A Python SDK shipped from OpenAPI in 2026 should be a typed pip install away from working. That means typing-aware, async-aware, and consistent enough that customers can read the method list and know how it'll behave. Three paths get you there: write it by hand, generate locally, or use a hosted pipeline. Here's the practical version of each.

Spec hygiene first

Same rules as TypeScript: every operation has an operationId (it becomes the method name), schemas live in components/schemas and are referenced (not inlined), every operation has an example, security schemes are defined and applied. A generator can't recover from a bad spec.

The Python-specific note: operationId becomes snake_case in idiomatic Python output. createUsercreate_user. Most generators handle this. Verify your generator does or your SDK feels foreign to Python developers.

See OpenAPI best practices for SDK-friendly specs for the full checklist.

Path 1: hand-written Python client

Best when you have ten endpoints, you control both API and SDK, and you want zero generator dependency.

from dataclasses import dataclass
from typing import Optional
import httpx

@dataclass
class ClientOptions:
    api_key: str
    base_url: str = "https://api.example.com"

class APIError(Exception):
    def __init__(self, status: int, body: str):
        self.status = status
        self.body = body
        super().__init__(f"API {status}: {body}")

class Client:
    def __init__(self, opts: ClientOptions):
        self._opts = opts
        self._http = httpx.Client(
            base_url=opts.base_url,
            headers={"Authorization": f"Bearer {opts.api_key}"},
            timeout=30,
        )

    def list_users(self) -> list[dict]:
        r = self._http.get("/users")
        if not r.is_success:
            raise APIError(r.status_code, r.text)
        return r.json()

Fine for a small API. Doesn't scale — adding 50 endpoints turns into a maintenance burden, and your spec/SDK drift the moment the spec changes.

Path 2: run a generator locally

Two real options:

  • openapi-python-client — modern, async-aware, produces a clean typed client with type-annotated dataclass models (older versions used attrs; current releases moved to dataclasses). Most Python-native option in 2026.
  • OpenAPI Generator (python template) — full codegen with sync + async variants. Heavier, more configurable, slower to adapt to OpenAPI 3.1 specifics.
# Approach A — modern python-native
pipx install openapi-python-client
openapi-python-client generate --path ./openapi.yaml

# Approach B — OpenAPI Generator
npx @openapitools/openapi-generator-cli generate \
  -i ./openapi.yaml \
  -g python \
  -o ./generated

Both work. Approach A produces something a Python developer reads and recognises immediately. Approach B produces something a Java developer would write — works fine, feels less native.

Path 3: hosted pipeline

Same story as TypeScript: upload spec, get an SDK, preview, publish. Bloom, Stainless, Speakeasy all do this.

With Bloom:

  1. Upload OpenAPI at signup. Dashboard shows parsed structure and any spec issues.
  2. Preview the generated Python SDK — method names, type definitions, async support, error classes, examples, all visible before any PyPI push.
  3. Compatibility report vs previously published SDK — breaking change detection on the public surface.
  4. Publish to PyPI from the dashboard or hand off the package for you to publish yourself.

Pricing: free for 30 days, no credit card. Enough to generate, preview, publish, and see if the output meets your bar before any monthly cost.

What "good" Python SDK output looks like

  • Method names match operationId in snake_case.
  • Types are explicit: dataclasses or Pydantic v2 models, with from __future__ import annotations so they work in older runtimes.
  • Errors are typed: APIError with status, code, message, body.
  • A single Client(options=...) constructor, plus per-resource accessors (client.users.list()).
  • Async support: every method has both list_users() and await list_users_async(), or the whole client is async-only with AsyncClient exposed separately.
  • Pagination has helpers, not raw cursor handling.
  • The package ships type hints visible to mypy/pyright — include py.typed in the package.

If the output is missing any of these, expect customer issues. Python developers in 2026 expect typing, and a typed SDK without py.typed looks unmaintained.

A note on async

OpenAPI doesn't model sync vs async — it's a transport concern. Most modern Python clients ship both: a Client that wraps httpx.Client (sync) and an AsyncClient that wraps httpx.AsyncClient (async). Some teams ship async-only to avoid maintaining two surfaces. Pick whichever matches your audience; web service backends are usually async, scripts and notebooks are usually sync.

openapi-python-client generates both by default. OpenAPI Generator's python template generates both. Hand-rolled SDKs usually ship one and add the other later.

Next: publishing and breaking-change detection

Two follow-ups worth reading:

Want spec → preview → PyPI as one workflow?

Try Bloom free for 30 days — upload your OpenAPI spec, preview the generated Python SDK in the dashboard, see a compatibility report against your last published version, and publish from the same workflow. Completely free for 30 days. No credit card required.