---
title: "Python SDK: install, clients, system_one()"
type: reference
tags: [python, sdk, client, install, system-one]
created: 2026-09-17
updated: 2026-09-17
confidence: high
sources:
  - raw/docs/sdk.md
  - raw/docs/sdk__python.md
  - raw/docs/sdk__python__usage.md
  - raw/docs/sdk__python__api.md
  - raw/docs/sdk__python__api__clients__sync__client.md
  - raw/docs/sdk__python__api__clients__async__client.md
  - raw/docs/sdk__python__api__clients__sync__models.md
  - raw/docs/sdk__python__api__clients__async__models.md
  - raw/docs/sdk__python__api__constants.md
  - raw/github/typesafe-sdk-python/README.md
  - raw/github/typesafe-sdk-python/pyproject.toml
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/__init__.py
jev_version: "jev-1.13.0"
sdk_python: "0.6.0"
summary: "typesafe-sdk 0.6.0: install, TypeSafeClient/AsyncTypeSafeClient constructor params, system_one() kwargs, models resource, env vars, logging, and the full export list."
---

# Python SDK: install, clients, system_one()

> **TL;DR** `pip install typesafe-sdk` (import name `typesafe_sdk`), set `TYPESAFE_API_KEY`, then `with TypeSafeClient() as client: client.system_one(state, questions)`. Both clients are keyword-only in their constructors; `system_one(state, questions, *, model, retry, timeout, extra_headers, extra_body)` returns a `SystemOneResponse`. The async twin is `AsyncTypeSafeClient` with `await client.system_one(...)` and `aclose()`.

## Install

| Tool | Command |
|---|---|
| uv | `uv add typesafe-sdk` |
| pip | `pip install typesafe-sdk` |

The distribution name is **`typesafe-sdk`**; the import name is **`typesafe_sdk`**.

Package-name traps:

- **`typesafe-ai`** on PyPI (version 0.1.0) is a *redirect shim* that simply depends on `typesafe-sdk`. Installing it works, but you still `import typesafe_sdk`. Prefer `typesafe-sdk` directly.
- **`typesafe`** 0.9.1 on PyPI is an unrelated third-party package and is **not** TypeSafe AI.
- The cookbook pages use a `pypi.typesafe.ai` index for a `cooksafe` helper; that host returned 404 publicly as of 2026-09-17.

Then set the API key (create one at `https://console.typesafe.ai/`):

```sh
export TYPESAFE_API_KEY=...
```

## Requirements

| Item | Value | Source |
|---|---|---|
| `requires-python` | `>=3.10` | `pyproject.toml` |
| Declared Python classifiers | 3.10, 3.11, 3.12, 3.13, 3.14 | `pyproject.toml` |
| License | MIT (`LICENSE` shipped) | `pyproject.toml` |
| Typing | `Typing :: Typed`, ships `py.typed` | `pyproject.toml`, `src/typesafe_sdk/py.typed` |
| Build backend | `uv_build>=0.12.5,<0.13` | `pyproject.toml` |
| Author / maintainer | TypeSafe AI `<support@typesafe.ai>` / Daniel Gafni `<daniel@typesafe.ai>` | `pyproject.toml` |

Runtime dependencies (`[project].dependencies`):

| Dependency | Constraint | Used for |
|---|---|---|
| `httpx2` | `>=2.0.0` | HTTP transport, `Timeout`, `Headers`, `Response` |
| `msgspec` | `>=0.21.1` | JSON encode/decode into `Struct` types |
| `tenacity` | `>=9.0.0` | retry loop (`Retrying` / `AsyncRetrying`) |
| `typing-extensions` | `>=4.13.0` | `Self`, `override`, `NotRequired`, `TypedDict` |

Project URLs: Homepage `https://typesafe.ai`, Documentation `https://docs.typesafe.ai/sdk/python/`, Changelog `https://docs.typesafe.ai/sdk/python/changelog/`, Repository `https://github.com/typesafe-ai/typesafe-sdk-python`, Issues `.../issues`.

## Public exports

`typesafe_sdk.__all__` (38 names, verbatim from `src/typesafe_sdk/__init__.py`):

| Group | Names |
|---|---|
| Clients | `TypeSafeClient`, `AsyncTypeSafeClient`, `Models`, `AsyncModels` |
| Questions | `Noul`, `Choice`, `Score`, `NoulCriteria`, `NoulModel`, `ChoiceModel`, `ScoreModel`, `QuestionModel`, `Question`, `Questions` |
| Responses | `SystemOneResponse`, `Answer`, `NoulAnswer`, `ChoiceAnswer`, `ScoreAnswer`, `Usage`, `ListModelsResponse`, `ModelMetadata` |
| JSON types | `JSONContent`, `JSONValue` |
| Retry | `RetryPolicy` |
| Errors | `TypeSafeError`, `TypeSafeAPIError`, `TypeSafeAPIConnectionError`, `TypeSafeAPITimeoutError`, `TypeSafeAPIResponseValidationError`, `TypeSafeAuthenticationError`, `TypeSafeBadRequestError`, `TypeSafeInternalServerError`, `TypeSafeNotFoundError`, `TypeSafePermissionDeniedError`, `TypeSafeRateLimitError`, `TypeSafeUnprocessableEntityError` |
| Submodule | `constants` |

`__version__` is also importable (`from typesafe_sdk import __version__`) although it is not listed in `__all__`; it is resolved at import time with `importlib.metadata.version("typesafe-sdk")`.

Everything else lives under `typesafe_sdk._core` / `typesafe_sdk._schemas` and is private. `__init__.py` ends with `del _core`, so `typesafe_sdk._core` is not bound as an attribute of the package after import even though the submodule itself is importable.

## Clients

Two clients, identical surface except for `async`/`await` and the transport types:

| | Sync | Async |
|---|---|---|
| Class | `TypeSafeClient` | `AsyncTypeSafeClient` |
| Call | `client.system_one(...)` | `await client.system_one(...)` |
| Models resource | `client.models` → `Models` | `client.models` → `AsyncModels` |
| List models | `client.models.list()` | `await client.models.list()` |
| Close | `close()` | `await aclose()` |
| Context manager | `with ... as client` | `async with ... as client` |
| `transport` type | `httpx2.BaseTransport` | `httpx2.AsyncBaseTransport` |
| `http_client` type | `httpx2.Client` | `httpx2.AsyncClient` |

### Constructor

Both constructors are **keyword-only** (`def __init__(self, *, ...)`); there are no positional parameters.

```python
TypeSafeClient(
    *,
    api_key: str | None = None,
    model: str | None = None,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    headers: Mapping[str, str] | None = None,
    transport: httpx2.BaseTransport | None = None,
    http_client: httpx2.Client | None = None,
    base_url: str | None = None,
)
```

```python
AsyncTypeSafeClient(
    *,
    api_key: str | None = None,
    model: str | None = None,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    headers: Mapping[str, str] | None = None,
    transport: httpx2.AsyncBaseTransport | None = None,
    http_client: httpx2.AsyncClient | None = None,
    base_url: str | None = None,
)
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `api_key` | `str \| None` | yes (or env) | `None` | API key. May be set via `TYPESAFE_API_KEY`. Missing key raises `TypeSafeError`. |
| `model` | `str \| None` | no | `None` → `TYPESAFE_DEFAULT_MODEL` → `"jev-latest"` | Default model for every call from this client. |
| `retry` | `RetryPolicy \| None` | no | `None` → `RetryPolicy()` defaults | Retry behavior. `RetryPolicy(max_retries=0)` disables retries. |
| `timeout` | `float \| httpx2.Timeout \| None` | no | `None` → `http_client.timeout` if `http_client` given, else `10.0` (`constants.DEFAULT_TIMEOUT`) | Timeout for each HTTP operation. Invalid values raise `TypeSafeError`. |
| `headers` | `Mapping[str, str] \| None` | no | `None` | Additional default request headers. |
| `transport` | `httpx2.BaseTransport` / `httpx2.AsyncBaseTransport` `\| None` | no | `None` | Custom transport, closed when the SDK client closes. Mutually exclusive with `http_client`. |
| `http_client` | `httpx2.Client` / `httpx2.AsyncClient` `\| None` | no | `None` | Bring your own HTTP client. Closed when the SDK client closes. Mutually exclusive with `transport`. |
| `base_url` | `str \| None` | no | `None` → `TYPESAFE_BASE_URL` → `https://api.typesafe.ai` | API root. Trailing `/` is stripped during resolution. |

Raises:

| Exception | When |
|---|---|
| `TypeSafeError` | No API key resolved, or `timeout` is not a positive finite number / `httpx2.Timeout`. |
| `ValueError` | Both `transport` and `http_client` supplied (`"transport and http_client are mutually exclusive."`). |

Resolution rules (from `_core/config.py`):

- Explicit arguments win over environment variables.
- Empty or whitespace-only environment values are ignored and fall back to the default.
- `base_url` is `rstrip("/")`-ed.
- `timeout` is validated by `resolve_timeout`: a non-`httpx2.Timeout` value must be finite and `> 0`.
- The resolved `api_key` is stored on a dataclass field with `repr=False`, as are the default headers, so it does not leak through `repr()`.

### Attributes and lifecycle

| Member | Kind | Type | Notes |
|---|---|---|---|
| `models` | `cached_property` | `Models` / `AsyncModels` | Built once per client instance. |
| `system_one(...)` | method / `async` method | → `SystemOneResponse` | See below. |
| `close()` / `aclose()` | method / `async` method | `None` | Closes the underlying HTTP client, **including one you supplied** via `http_client`. |
| `__enter__` / `__exit__` | context manager | — | Sync client only. |
| `__aenter__` / `__aexit__` | async context manager | — | Async client only. |

Context-manager usage is the documented default in every upstream example. Because `close()`/`aclose()` also close a user-supplied `http_client`, do not share one `httpx2.Client` across several `TypeSafeClient` instances whose lifetimes differ.

## `system_one()`

```python
system_one(
    state: JSONContent,
    questions: Mapping[str, Question],
    *,
    model: str | None = None,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    extra_headers: Mapping[str, str] | None = None,
    extra_body: Mapping[str, JSONValue | None] | None = None,
) -> SystemOneResponse
```

The async version has the same signature and is `async def`, returning `SystemOneResponse` when awaited.

| Parameter | Type | Positional? | Default | Description |
|---|---|---|---|---|
| `state` | `JSONContent` (`str \| Mapping[str, JSONValue \| None] \| Sequence[JSONValue \| None]`) | yes (1st) | — | Text, a JSON object, or an array to evaluate. Cannot be `None`; values *inside* an object may be `None`. |
| `questions` | `Mapping[str, Question]` | yes (2nd) | — | Nonempty mapping of your names → question objects or raw dicts. |
| `model` | `str \| None` | keyword-only | `None` | Per-call model override; `None` inherits the client default. |
| `retry` | `RetryPolicy \| None` | keyword-only | `None` | Per-call retry policy, replacing the client-level one for this call. |
| `timeout` | `float \| httpx2.Timeout \| None` | keyword-only | `None` | Per-call HTTP timeout in seconds; `None` inherits the client value. |
| `extra_headers` | `Mapping[str, str] \| None` | keyword-only | `None` | Extra request headers for this call. |
| `extra_body` | `Mapping[str, JSONValue \| None] \| None` | keyword-only | `None` | Extra top-level body fields, shallow-merged **over** the body after `state`, `model`, `questions` are set. Last write wins; object values are replaced, not deep-merged. |

Returns `SystemOneResponse` — see [[reference/python-sdk-responses]].

Raises:

| Exception | When |
|---|---|
| `TypeSafeError` | `questions` is empty; a `Score` question's `criteria` is empty; a dict question lacks a nonempty string `"type"`; a `"choice"`/`"score"` dict question has no `"criteria"` key; or the body cannot be JSON-encoded. |
| `TypeSafeAPIError` (and subclasses) | The server returned an unsuccessful HTTP status after any retries. |
| `TypeSafeAPIConnectionError` / `TypeSafeAPITimeoutError` | The request could not connect, or timed out, after any retries. |
| `TypeSafeAPIResponseValidationError` | A 2xx response whose body was missing or structurally invalid. |

> The docs' "Raises" block for `system_one` lists only the empty-questions and empty-score-criteria cases; `_core/questions.py` additionally raises `TypeSafeError` for a malformed question dictionary and for a `choice`/`score` dict with no `"criteria"` key, and `_core/transport.py` raises it when the body cannot be encoded as JSON.

### Wire request built

`_core/endpoints.py:prepare_system_one` sends `POST {base_url}/v1/systemone` with the body:

```json
{"state": ..., "model": "...", "questions": {...}}
```

`model` is always present (client default when the per-call override is `None`), then `extra_body` is applied with `body.update(extra_body)`. See [[reference/http-api]] for the wire contract.

### Headers the SDK sets

Set on every request from `_core/transport.py:prepare` (user `headers`/`extra_headers` are merged first, then these overwrite them — so authentication, `Accept`, and SDK identification cannot be overridden):

| Header | Value |
|---|---|
| `Authorization` | `Bearer {api_key}` |
| `Accept` | `application/json` |
| `User-Agent` | `typesafe-sdk/{__version__}` |
| `X-TypeSafe-SDK` | `typesafe-sdk/{__version__}` |
| `X-TypeSafe-Runtime` | `python/{platform.python_version()} ({sys.platform}; {platform.machine()})` |
| `Content-Type` | `application/json` (only when a body is sent) |
| `X-TypeSafe-Retry-Count` | attempt number, added only on retries; any caller-supplied value is dropped first |

The response header `x-typesafe-request-id` is surfaced as `response.request_id` and `error.request_id`.

## Environment variables

| Variable | Configures | Default | Constant |
|---|---|---|---|
| `TYPESAFE_API_KEY` | API key (required) | — | `constants.API_KEY_ENV` |
| `TYPESAFE_BASE_URL` | API root URL | `https://api.typesafe.ai` | `constants.BASE_URL_ENV` / `DEFAULT_BASE_URL` |
| `TYPESAFE_DEFAULT_MODEL` | Default model | `jev-latest` | `constants.DEFAULT_MODEL_ENV` / `DEFAULT_MODEL` |
| `TYPESAFE_LOG_LEVEL` | `typesafe_sdk` logger level, applied once at import | unset | `constants.LOG_LEVEL_ENV` |

See [[reference/environment-variables]] and [[reference/python-sdk-retries-errors]] for the constants module in full.

## `models` resource

`client.models` is a cached property returning `Models` (sync) or `AsyncModels` (async). It has one method:

```python
list(
    *,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> ListModelsResponse
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `retry` | `RetryPolicy \| None` | `None` | Per-call retry override. |
| `timeout` | `float \| httpx2.Timeout \| None` | `None` | Per-operation timeout override; `None` inherits the client setting. |
| `extra_headers` | `Mapping[str, str] \| None` | `None` | Extra headers; authentication, SDK identification, and `Accept` remain protected. |

Issues `GET {base_url}/v1/models`. Returns `ListModelsResponse` whose `.models` is a `tuple[ModelMetadata, ...]` of `name`, `description`, `release_date` — see [[reference/python-sdk-responses]] and [[reference/models-and-pricing]].

```python
from typesafe_sdk import TypeSafeClient

with TypeSafeClient() as client:
    for card in client.models.list().models:
        print(card.name, card.release_date, card.description)
```

Select a model when constructing a client:

```python
client = TypeSafeClient(model="jev")
```

Note: `"jev"` is the upstream usage-guide sample verbatim (raw/docs/sdk__python__usage.md), but it is not among the names listed on [[reference/models-and-pricing]] (`jev-latest`, `jev-preview`, `jev-1.13.0`). Prefer `model="jev-latest"` or an explicit versioned id, and confirm with `client.models.list()`.

## Logging

The SDK logs to the `typesafe_sdk` logger (`logging.getLogger("typesafe_sdk")`) and attaches a `NullHandler` plus a `SensitiveHeadersFilter`. It never configures handlers for you.

```python
import logging

logging.getLogger("typesafe_sdk").setLevel(logging.DEBUG)
```

Or set `TYPESAFE_LOG_LEVEL` **before importing the SDK**; it is applied once at import.

| Level string | Effect |
|---|---|
| `debug` | `logging.DEBUG` — also logs request and response headers and bodies |
| `info` | `logging.INFO` — one summary line per request (`METHOD url <- status in Nms (request <id>)`) plus a line per retry |
| `warn` | `logging.WARNING` (accepted by the source; not listed in the docs page) |
| `warning` | `logging.WARNING` |
| `error` | `logging.ERROR` |
| `off` | `logging.CRITICAL + 1` |

Redaction: header names in `{authorization, proxy-authorization, x-api-key, api-key, cookie, set-cookie}`, plus any header name containing `token` or `secret` (case-insensitive), are replaced with `***`. **Request and response bodies are not redacted** — `debug` will print your `state` and the model's answers.

The SDK also logs `Ignoring answer %r with unrecognized type %r` at WARNING when the API returns an answer kind this version does not model.

## Forward compatibility

| Need | Mechanism |
|---|---|
| Send a request field newer than the SDK | `extra_body={"beam_width": 4}` |
| Send a question field newer than the SDK | Pass the question as a plain dict: `{"type": "noul", "instructions": "...", "weight": 2}` |
| Read an answer kind newer than the SDK | The SDK logs a warning, skips it, and you read `result.raw_http_response.json()["answers"]` |
| Unknown extra fields on known responses | Silently ignored (`forbid_unknown_fields=False`) |

```python
from typesafe_sdk import Noul, TypeSafeClient

with TypeSafeClient() as client:
    client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="About billing?")},
        extra_body={"beam_width": 4},
    )
```

## Complete examples

Sync, verbatim style from the upstream quickstart:

```python
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

with TypeSafeClient() as client:
    response = client.system_one(
        state={"document": "I was charged twice. Please fix this ASAP."},
        questions={
            "billing": Noul(instructions="Is this ticket about billing?"),
            "tone": Choice(
                instructions="What is the customer's tone?",
                criteria={"calm": None, "frustrated": None, "angry": None},
            ),
            "urgency": Score(
                instructions="How urgent is this ticket?",
                criteria=["can wait", "this week", "today"],
            ),
        },
    )

print(response.nouls["billing"].noul)
print(response.choices["tone"].choice)
print(response.scores["urgency"].score)
```

Async:

```python
import asyncio

from typesafe_sdk import AsyncTypeSafeClient, Choice, Noul, Score


async def main() -> None:
    async with AsyncTypeSafeClient() as client:
        result = await client.system_one(
            "I was charged twice. Please help ASAP.",
            {
                "billing": Noul(instructions="Is this about billing?"),
                "tone": Choice(
                    instructions="What is the tone?",
                    criteria={"calm": None, "angry": None},
                ),
                "urgency": Score(
                    instructions="How urgent is this?",
                    criteria=["low", "medium", "high"],
                ),
            },
        )
        print(
            result.nouls["billing"].noul,
            result.choices["tone"].choice,
            result.scores["urgency"].score,
        )


asyncio.run(main())
```

Per-call overrides plus error handling:

```python
from typesafe_sdk import Noul, RetryPolicy, TypeSafeAPIError, TypeSafeClient

with TypeSafeClient(model="jev") as client:
    try:
        result = client.system_one(
            "I was charged twice.",
            {"billing": Noul(instructions="Is this about billing?")},
            model="jev-latest",
            retry=RetryPolicy(max_retries=3, backoff_max=0.2, timeout=1.0),
            timeout=5.0,
            extra_headers={"X-Request-Source": "support-bot"},
        )
    except TypeSafeAPIError as error:
        print(error.status, error.request_id)
    else:
        print(result.model, result.usage.input_tokens, result.request_id)
```

## When to use / when not to use

- Use `TypeSafeClient` for scripts, sync web frameworks, and notebooks; use `AsyncTypeSafeClient` inside asyncio services and when fanning out many calls concurrently (see [[patterns/fan-out]]).
- Do not create a client per request: construction resolves config and builds an `httpx2` client. Build one per process and reuse it.
- If you only need one call in one language-agnostic place, the raw [[reference/http-api]] is equivalent; the SDK adds typed questions/answers and the default retry policy.

## Version notes

- Version documented here: `typesafe-sdk` **0.6.0** (repo commit `420ef4ffb612d5a539a1e0f0fe883ff6770340af`, 2026-09-15). See [[reference/python-sdk-changelog]].
- 0.6.0 changed `Score.criteria` from an int-keyed dict to an ordered sequence — see [[reference/python-sdk-questions]].
- `Usage.billing_units` exists in the generated wire schema but **not** on the public `Usage` type; details in [[reference/python-sdk-responses]].

## Related

- [[reference/python-sdk-questions]] — `Noul`, `Choice`, `Score` and their dict forms
- [[reference/python-sdk-responses]] — `SystemOneResponse`, answers, usage, models
- [[reference/python-sdk-retries-errors]] — `RetryPolicy`, exceptions, constants
- [[reference/python-sdk-changelog]] — release history
- [[reference/http-api]] — the wire contract the SDK speaks
- [[reference/environment-variables]] — `TYPESAFE_*` across SDKs
- [[reference/javascript-sdk]] — the JS/TS equivalent
- [[reference/system-one-adapter]] — LLM-backed drop-in for `TypeSafeClient`
- [[guides/quickstart]] — first call in HTTP, Python, JS
- [[concepts/state]] — what `state` may contain
- [[concepts/primitives]] — Choice, Score, Noul

## Sources

- raw/docs/sdk.md (https://docs.typesafe.ai/sdk.md)
- raw/docs/sdk__python.md (https://docs.typesafe.ai/sdk/python.md)
- raw/docs/sdk__python__usage.md (https://docs.typesafe.ai/sdk/python/usage.md)
- raw/docs/sdk__python__api.md (https://docs.typesafe.ai/sdk/python/api.md)
- raw/docs/sdk__python__api__clients__sync__client.md (https://docs.typesafe.ai/sdk/python/api/clients/sync/client.md)
- raw/docs/sdk__python__api__clients__async__client.md (https://docs.typesafe.ai/sdk/python/api/clients/async/client.md)
- raw/docs/sdk__python__api__clients__sync__models.md (https://docs.typesafe.ai/sdk/python/api/clients/sync/models.md)
- raw/docs/sdk__python__api__clients__async__models.md (https://docs.typesafe.ai/sdk/python/api/clients/async/models.md)
- raw/docs/sdk__python__api__constants.md (https://docs.typesafe.ai/sdk/python/api/constants.md)
- raw/github/typesafe-sdk-python/README.md, pyproject.toml, src/typesafe_sdk/__init__.py, src/typesafe_sdk/_core/{config,transport,endpoints,logging,constants}.py (https://github.com/typesafe-ai/typesafe-sdk-python @ 420ef4ffb612d5a539a1e0f0fe883ff6770340af)
- PyPI package facts (`typesafe-sdk`, `typesafe-ai` shim, unrelated `typesafe`) collected 2026-09-17 and recorded in CLAUDE.md
