Generating a Python SDK from an OpenAPI Specification
If your API ships an OpenAPI spec, OpenAPI Generator can build the Python client from it. You don't have to hand-write models and endpoint wrappers.
Most tutorials stop at generate. After that you're still choosing generator options, picking a spec with enough surface area to matter, locking dependencies, running smoke tests, and keeping the repo sane when the spec changes again.
This walkthrough uses a compact OpenAPI 3.1 example. I use Astral uv for lock, sync, test, and build because that's what I run locally. The generator config and generate steps are the same with pip or Poetry.
Prerequisites¶
You need three things:
openapi-generator-cliRefer to theopenapi-generator-clidocumentation for installation instructions.- Java, because the generator runs on the JVM
uv, for the lock, sync, test, and build steps in this walkthrough Refer to the A Modern Python Workflow with Astraluvarticle for installation instructions.
Start with a Useful Specification¶
Start with an API specification that'll actually exercise the generator: tags, pagination, request bodies, enums, timestamps, and a reusable error schema, not one of the generic "Pet Store" examples.
Here is a compact OpenAPI 3.1 example:
openapi: 3.1.0
info:
title: Dispatch API
version: 1.0.0
summary: A compact task-tracking API for SDK generation examples.
description: |
This example stays small enough for a blog post while still looking like a real API.
It includes pagination, reusable schemas, enums, timestamps, bearer auth, and a
structured error response.
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
servers:
- url: https://api.example.com/v1
description: Production
tags:
- name: Tasks
description: Manage tracked work items.
paths:
/tasks:
get:
tags: [Tasks]
operationId: listTasks
summary: List tasks
parameters:
- name: status
in: query
required: false
description: Filter tasks by workflow state.
schema:
$ref: "#/components/schemas/TaskStatus"
- name: limit
in: query
required: false
description: Maximum number of tasks to return.
schema:
type: integer
minimum: 1
maximum: 100
default: 25
- name: cursor
in: query
required: false
description: Opaque pagination cursor from a previous response.
schema:
type: string
responses:
"200":
description: A page of tasks.
content:
application/json:
schema:
$ref: "#/components/schemas/TaskListResponse"
"401":
description: Authentication failed.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags: [Tasks]
operationId: createTask
summary: Create a task
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTaskRequest"
responses:
"201":
description: Task created.
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
"400":
description: Invalid request body.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/tasks/{taskId}:
get:
tags: [Tasks]
operationId: getTask
summary: Get a task by ID
parameters:
- $ref: "#/components/parameters/TaskId"
responses:
"200":
description: The requested task.
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
"404":
description: Task not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags: [Tasks]
operationId: updateTask
summary: Update a task
parameters:
- $ref: "#/components/parameters/TaskId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateTaskRequest"
responses:
"200":
description: Task updated.
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
"404":
description: Task not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
TaskId:
name: taskId
in: path
required: true
description: Stable task identifier.
schema:
type: string
format: uuid
schemas:
TaskStatus:
type: string
enum:
- queued
- running
- completed
- failed
Task:
type: object
required:
- id
- title
- status
- createdAt
- updatedAt
properties:
id:
type: string
format: uuid
title:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
status:
$ref: "#/components/schemas/TaskStatus"
labels:
type: array
items:
type: string
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateTaskRequest:
type: object
required:
- title
properties:
title:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
labels:
type: array
items:
type: string
UpdateTaskRequest:
type: object
properties:
title:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
status:
$ref: "#/components/schemas/TaskStatus"
labels:
type: array
items:
type: string
TaskListResponse:
type: object
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/Task"
nextCursor:
type:
- string
- "null"
Error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
requestId:
type:
- string
- "null"
security:
- bearerAuth: []
This example is small enough to walk through without pasting pages of YAML.
Configure the Python Generator¶
Create a generator configuration file and make the build system choice explicit:
{
"packageName": "python_dispatch_sdk",
"projectName": "pythn-dispatch-sdk",
"packageVersion": "1.0.0",
"packageUrl": "https://example.com/dispatch-sdk",
"description": "Python SDK for the Dispatch API.",
"author": "The Authors",
"authorEmail": "[email protected]",
"license": "MIT",
"library": "httpx",
"buildSystem": "hatchling",
"hideGenerationTimestamp": true
}
In that configuration, these fields matter most:
packageName: the import name, so usesnake_caseprojectName: the distribution name, so use hyphenslibrary: httpx: the default I'd pick today unless you needurllib3(see Requests vs. HTTPX)buildSystem: hatchling: keeps the generatedpyproject.tomlaligned with current Python packaging practicehideGenerationTimestamp: true: removes a useless source of churn from regenerated files
Don't flip disallowAdditionalPropertiesIfNotPresent without reading the docs first. OpenAPI Generator's Python generator docs call true the old behavior and false the spec-compliant one. It's a compatibility switch, not a free hardening flag.
Generate the SDK¶
With the spec and config in place, generate the client:
openapi-generator-cli generate \
-i snippets/example-openapi-3.1-dispatch-api.yaml \
-g python \
-o ./client \
-c snippets/openapi-generator-config-example.json
That produces a Python package, API classes, models, tests, and a pyproject.toml. The point is to treat that output as generated source, not something you hand-edit line by line.
When the upstream specification changes, rerun the same command and review the diff. If you're hand-editing generated files, you'll fight every regeneration.
Manage the Generated Package with uv¶
The generator hands you a pyproject.toml. Below I use uv to compile a lockfile, sync the environment, run tests, and build artifacts. Swap in pip or Poetry if that's what you already use.
Lock the generated dependencies:
Create a local environment and sync it exactly to the lockfile:
On Windows, activate with .venv\Scripts\activate instead.
Install the generated package plus test tools:
uv pip compile writes a lock you can review in requirements.txt. uv pip sync installs exactly that lock instead of whatever was already on the machine.
Build the Distribution¶
Once the smoke tests pass, build the package artifacts:
That writes the source distribution and wheel into dist/.
Keep Your Repository Clean¶
Generated SDK repositories can collect a lot of junk unless you make the boundaries explicit.
At a minimum, ignore local environments, caches, build output, and generator metadata:
.venv/
__pycache__/
*.py[cod]
dist/
build/
.pytest_cache/
.coverage
htmlcov/
.openapi-generator/
git_push.sh
I'd also rewrite the generated README.md before publishing. The scaffold is fine for a first pass, but it's not documentation you should put your name on.
Optional: Template Overrides¶
If you need custom copyright or SPDX headers, use template overrides instead of editing generated files after the fact:
From there, edit the copied Mustache templates and regenerate with -t ./templates. Keep the override directory small. The more templates you fork, the more work you'll inherit on generator upgrades.
Here is the generated workflow in one pass:

Let the generator handle the scaffolding.
The specification quality, package boundaries, and how you ship releases are where the maintenance cost actually lives.
References¶
- OpenAPI Generator Python Generator Documentaton
- OpenAPI Generator Configuration dDocumentaton
- A Modern Python Workflow with Astral
uv