Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /non-typescript-codegen.md.
Verified · 8/11/2026

Non-TypeScript Codegen: Go and Python Clients

Shows how the Go SDK's ogen client and the Python SDK's openapi-python-client client are both generated from the same normalized OpenAPI spec, and what post-processing each language applies before the output ships.

The Go SDK and Python SDK don't hand-write their API clients. Both generate from the same normalized OpenAPI document that OpenAPI Spec Normalization and Codegen Artifacts produces, using an off-the-shelf generator for their language, then run a small fixup pass that repairs generator quirks the platform's wire contract doesn't tolerate.

Reach for this page when you're regenerating the Go or Python client after an openapi/primitive-api.yaml change, or debugging why generated code in sdk-go/api or sdk-python/src/primitive/api looks different from what you expected.

What each language generates with#

Go generates its client with ogen into sdk-go/api, and Python generates with openapi-python-client into sdk-python/src/primitive/api. Both generators consume openapi/primitive-api.codegen.json, the OpenAPI 3.0.3 build artifact normalized from the hand-written 3.1 spec (never edit that JSON file directly; see OpenAPI Spec Normalization and Codegen Artifacts).

SDKGeneratorOutput locationClient type
Googensdk-go/apiGenerated Go package, imported as primitiveapi
Pythonopenapi-python-clientsdk-python/src/primitive/apiGenerated Python package, imported as primitive.api

The Node SDK's counterpart is the workspace-internal @primitivedotdev/api-core package (see What is API Core?); Go and Python have no equivalent shared package, so each generates and post-processes its client directly inside its own SDK directory.

Tip

For day-to-day app code, use the high-level client.send/reply/forward surface, not the generated client directly. Reach for the generated client only for operations the high-level client doesn't cover, see Generated API Client for the Python surface.

Regenerating the Go client#

Run make go-generate from the repo root, then make go-check and commit the regenerated sdk-go/api files with your spec change.

  1. 1

    Run the Go generate target#

    From the repo root:

    make go-generate
    

    This regenerates sdk-go/api from openapi/primitive-api.codegen.json using ogen.

  2. 2

    Run the Go SDK's checks#

    make go-check
    

    The Go SDK's own checks are go test ./..., the shared-fixture run go test -run TestSharedCompatibilityFixtures ./..., and gofmt -w .. See Go SDK Testing and Development for the full local workflow.

  3. 3

    Commit the regenerated output#

    Commit the changed files under sdk-go/api alongside your spec change. ogen's output is deterministic for a given spec, so a diff here should map directly to your OpenAPI change.

Regenerating the Python client#

Run make python-generate from the repo root; it invokes sdk-python/scripts/generate_api_client.py, which regenerates sdk-python/src/primitive/api and applies three fixups.

Python's generation script, sdk-python/scripts/generate_api_client.py, wraps the openapi-python-client CLI and then applies three fixups the raw generator output needs before it's usable.

  1. 1

    Run the Python generate target#

    From the repo root:

    make python-generate
    

    Under the hood this invokes generate_api_client.py, which:

    1. Runs openapi-python-client generate --meta none --config openapi-python-client-config.yml --path openapi/primitive-api.codegen.json --output-path <temp dir> from the sdk-python directory.
    2. Deletes the previous generated api/, client.py, errors.py, models/, and types.py under sdk-python/src/primitive/api.
    3. Copies the freshly generated versions into their place.
    4. Applies the three post-processing fixups described below.
  2. 2

    Run the Python SDK's checks#

    make python-check
    

    The Python SDK's own checks are uv run pytest, uv run ruff check ., and uv run basedpyright.

  3. 3

    Commit the regenerated output#

    Commit the changed files under sdk-python/src/primitive/api alongside your spec change.

Python's three fixups#

The three fixups deduplicate repeated imports, guard the Content-Type header on optional-body operations, and switch file responses to raw bytes. generate_api_client.py runs each of these over every generated .py file after copying, in this order.

Deduplicate imports#

openapi-python-client 0.28.3 occasionally emits the same from X import ... line twice, for example from ..types import UNSET, Unset inside a 201-response model. dedupe_imports strips repeated module-level import lines (matched by a regex that only touches unindented from ... import ... lines, so imports inside TYPE_CHECKING blocks or function bodies are left alone). The duplicates are semantically harmless but trip strict linters downstream.

Guard the Content-Type header on optional-body operations#

openapi-python-client 0.28.3 unconditionally emits:

headers["Content-Type"] = "application/json"

even for operations whose request body is optional. When a caller omits the body, the generated code still sends Content-Type: application/json with no body, which trips middleware expecting either both or neither. guard_optional_body_content_type re-indents that assignment so it fires only inside the existing if not isinstance(body, Unset): block, matching this generator pattern exactly:

if not isinstance(body, Unset):
    _kwargs["json"] = body.to_dict()

headers["Content-Type"] = "application/json"

becomes:

if not isinstance(body, Unset):
    _kwargs["json"] = body.to_dict()
    headers["Content-Type"] = "application/json"

This is the Python-side counterpart to the same class of bug the Node SDK fixes in generated TypeScript, see Generated TypeScript Client Fixups, and it's also the failure mode covered in Codegen Troubleshooting for mismatched Content-Type headers on optional-body requests.

Use raw bytes for file/download responses#

File payloads wrap BytesIO, which requires bytes, not str. use_bytes_for_file_responses replaces every occurrence of BytesIO(response.text) with BytesIO(response.content) across the generated tree, so binary downloads (raw email .eml bytes, attachment bundles) decode correctly instead of being mangled through text decoding first.

Warning

Never hand-edit files under sdk-python/src/primitive/api or sdk-go/api. They're regenerated wholesale on every make python-generate / make go-generate run, and manual edits are silently discarded. Fix the spec, the generator config, or the fixup script instead.

Verifying the regeneration worked#

Build and test each regenerated SDK, and confirm both languages show a matching diff for the same spec change. After regenerating either client, confirm:

  • Go: go build ./... succeeds from sdk-go/, and go test -run TestSharedCompatibilityFixtures ./... passes.
  • Python: uv run basedpyright reports no new type errors, and uv run pytest passes.

A spec change that adds or removes an operation should show up as a corresponding diff in both sdk-go/api and sdk-python/src/primitive/api. If only one language's output changed, the other generator run was likely skipped.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer