---
title: "Python"
description: "Generate synchronous and asynchronous Python clients for your API with typed models and errors."
url: https://typeship.dev/docs/targets/sdk/python
markdown: https://typeship.dev/docs/targets/sdk/python.md
section: "Get started"
---
> ## Documentation index
> Fetch the complete documentation index at https://typeship.dev/llms.txt (every page, one line each) or the full text at https://typeship.dev/llms-full.txt.
> Append .md to any docs URL, or send Accept: text/markdown, for the markdown twin of that page.

# Python

Generate synchronous and asynchronous Python clients for your API with typed models and errors.

Typeship generates synchronous and asynchronous Python clients for your API as one focused package with no runtime dependencies. This page covers Python-specific behavior; shared client behavior is on the [SDK overview](https://typeship.dev/docs/targets/sdk).

CLI and MCP packages are generated and released separately.

## Package

* The distribution and import name derive from your API's title. The Acme API produces `acme`, installed with `pip install .` from the package directory or from PyPI once you [publish it](https://typeship.dev/docs/guides/publish). Set a different name per project in the console under **package names**.
* `pyproject.toml` declares `dependencies = []` and `requires-python >= 3.11`. The runtime is `urllib` only.
* The package ships `py.typed`, so type checkers see every `TypedDict` and `Literal`.

## Idioms

* **Raise, don't return.** Every failure is an exception under `TypeshipError`. See [Results and errors](https://typeship.dev/docs/targets/sdk#results-and-errors).
* **Keyword arguments for bodies.** When an operation's request body is an inline object (JSON or form-encoded), its fields become keyword arguments: `client.accounts.create(name="Ops", currency="usd")`. When the body is a named schema, an array, or plain text, it stays one `body=` argument typed accordingly (`AccountWrite`, `List[AccountWrite]`, `str`). If a body field is spelled like a path or query parameter, the whole body steps aside into `body=` rather than producing a duplicate argument.
* **Dicts, typed.** Responses are the parsed JSON, typed as `TypedDict`s. You read `account["id"]`, not `account.id`. There is no conversion layer between you and the wire.
* **Directional models stay honest.** A component whose request and response contracts differ becomes `AccountWrite` and `AccountRead`; identical contracts keep the clean `Account` name. Response enums accept future strings and discriminator responses retain an unknown `Dict[str, Any]` fallback, while request types remain closed.
* **Generators for pages.** `for account in client.accounts.list():` walks every page. `client.accounts.list_page()` returns one raw envelope.
* **`request_options` per call.** A dict with `timeout`, `max_retries`, and `headers`.

## Environment variables

The client reads these when the matching option is not passed:

| Variable           | Sets           |
| ------------------ | -------------- |
| `ACME_TOKEN`       | `bearer_token` |
| `ACME_BASE_URL`    | `base_url`     |
| `ACME_WEBHOOK_KEY` | `webhook_key`  |
| `ACME_DEBUG=1`     | `debug`        |

A single API key is `api_key=` and `ACME_API_KEY`, the same spelling as the TypeScript SDK and the CLI; several keys get one keyword and variable each, named after the header or parameter. Basic auth reads `ACME_USERNAME` and `ACME_PASSWORD`. The prefix comes from your package name.

## Transport

Pass `transport=` to replace `urllib`. A transport is a callable that takes `(method, url, headers, body_bytes, timeout)` and returns `(status, headers, body_bytes)`. Use it to route through a proxy, a recording layer, or a test double. Streaming endpoints read the response incrementally and go straight to `urllib`, so they bypass a custom transport.

## Webhooks

`client.webhooks.unwrap(payload, headers)` verifies and parses. `unwrap_unsafe` parses only. Module-level `verify_webhook` and `sign_webhook` are exported for handlers that do not hold a client. Events are `TypedDict`s discriminated by their `type` field. `WEBHOOK_NAMES` and `WEBHOOK_SAMPLES` list every declared event with a sample payload.

## Async

`AsyncAcmeClient` takes the same options and has the same methods, awaitable: `await client.accounts.get(...)`, and `async for` over paginated methods and streams. Requests run on the event loop's default executor, so nothing blocks the loop and there is still nothing to install. Both clients are context managers (`with` / `async with`) and have `close()` / `aclose()`.

The zero-dependency tradeoff is explicit: cancelling a coroutine stops waiting for its result, but it cannot interrupt a synchronous socket call already running in the executor. `timeout` bounds each socket attempt; it is not one wall-clock deadline across retries. The generated async client is therefore not the right transport when immediate in-flight socket cancellation is a hard requirement.

## Uploads

A `multipart/form-data` field is a keyword argument that accepts raw `bytes`, an open binary file (its name becomes the filename), or a `(filename, data, content_type)` tuple; the other fields of the body travel alongside as form parts. A raw binary body (`application/octet-stream`, `image/png`, ...) is `body=` with `bytes` or a file object, sent under the spec's content type.

## GraphQL

A GraphQL schema generates one method per query and mutation field, keyed under `client.query` and `client.mutation`. Arguments are keyword arguments and become variables. Every scalar field to depth 2 is selected by default, with a fragment per concrete type for unions and interfaces, and `select="{ id name }"` overrides that with a raw selection set on methods that return an object. Connection fields paginate like everything else. Errors in a `200` raise `GraphQLRequestError` (an `ApiError`) with the raw `errors` list.

## Connections

The default transport pools keep-alive connections per client on `http.client`, one per scheme/host/port, honoring `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`. A request that fails on a reused connection before any response arrived is retried once on a fresh one (the server closed an idle keep-alive, the same case Go's `net/http` retries), so pooling never turns into spurious transport errors. Redirects follow the rules `urllib` applied.

## Sitemap

[Every page of these docs](https://typeship.dev/llms.txt)
