---
title: "Python SDK internals: headers, key validation, gateways and base URLs, logging, forward compatibility, dependencies, migration notes"
type: reference
tags: [python, sdk, internals, logging, gateways, dependencies]
created: 2026-09-22
updated: 2026-09-22
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.md
  - raw/docs/sdk__python__api__clients__async.md
  - raw/docs/sdk__python__api__constants.md
  - raw/docs/sdk__python__changelog.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
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/config.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/transport.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/endpoints.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/json.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/logging.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/constants.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/schemas/base.py
jev_version: "jev-1.13.0"
sdk_python: "0.7.1"
summary: "typesafe-sdk 0.7.1 internals: full constructors, key validation, headers, wire body, env vars, models resource, AI gateways, logging, forward compatibility, dependencies, exports, 0.6 migration."
---

# Python SDK internals: headers, key validation, gateways and base URLs, logging, forward compatibility, dependencies, migration notes

> **TL;DR** The detail behind the builder contract on [[reference/python-sdk]]: full constructor signatures and config resolution, the 0.7.1 API-key validation, the headers and wire body the SDK sends, the `models` resource, `TYPESAFE_*` variables, pointing `base_url` at OpenRouter or Vercel AI Gateway, logging and redaction, forward-compatibility escape hatches, runtime dependencies, the export list, `response_model` mechanics, and 0.6.0 → 0.7.x migration notes. Documents `typesafe-sdk` 0.7.1. If you just need to make a call, read [[reference/python-sdk]] instead.

## 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` |
| `pydantic` | `>=2.12.0` | question/response models (`BaseModel`, `ConfigDict`, `ValidationError`) |
| `pydantic-core` | `>=2.41.1` | JSON codec (`to_json` / `from_json`) in `_core/json.py` |
| `tenacity` | `>=9.0.0` | retry loop (`Retrying` / `AsyncRetrying`) |
| `typing-extensions` | `>=4.13.0` | `Self`, `override`, `NotRequired`, `TypedDict`, `TypeAliasType` |

`msgspec>=0.21.1` was a runtime dependency up to 0.6.0 and is **gone** as of 0.7.0; the SDK no longer imports `msgspec` anywhere.

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 in full

| | 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 signatures

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,
)
```

`api_key`, `model`, `retry`, `timeout` and `base_url` are tabled on [[reference/python-sdk]]. The remaining three:

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `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`. |

Raises:

| Exception | When |
|---|---|
| `TypeSafeError` | No API key resolved, the API key is invalid, 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`. With `timeout=None` and an `http_client` supplied, the client's own `http_client.timeout` is used; otherwise `10.0` (`constants.DEFAULT_TIMEOUT`).
- 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 \| ResponseT` | `ResponseT` only when `response_model` is passed. |
| `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.

## API key validation (0.7.1)

`resolve_and_validate_api_key` in `_core/config.py` runs during `Config.create`, i.e. inside the constructor:

| Rule | Behavior |
|---|---|
| Whitespace | Stripped from both ends, "including newlines from key files". |
| Empty after stripping | `TypeSafeError("No API key was provided. Pass api_key or set the TYPESAFE_API_KEY environment variable.")` |
| Non-ASCII, non-printable, or containing a space | `TypeSafeError("API key must contain only printable ASCII characters without whitespace.")` |
| Explicitly empty `api_key=""` | Does **not** fall back to the environment (the docs state: "An explicitly empty key does not fall back to the environment."). |

The usage guide states the consequence plainly: "Invalid API keys raise `TypeSafeError` during client creation, before any request or retry."

Also new in 0.7.1: credentials are stripped from exception text. `_core/logging.py:redact_exception` copies a transport exception's message, chain, and notes with every secret-header value (and the credential part of `Authorization` / `Proxy-Authorization`) replaced by `***`, in raw, `repr`-escaped, byte-`repr`, and JSON-escaped forms, and detaches the unredacted original from `__context__` before re-raising as `TypeSafeAPIConnectionError` / `TypeSafeAPITimeoutError`. Step-by-step table in [[reference/python-sdk-retries-errors]].

## `system_one()` internals

The signature and parameter table are on [[reference/python-sdk]]. `state` is `JSONContent` (`str | Mapping[str, JSONValue | None] | Sequence[JSONValue | None]`). Returns, verbatim: "An instance of `response_model`, or `SystemOneResponse` with answers keyed by question name and model and token usage details when no custom model is supplied." It is published as two `@overload`s so the return type is exact:

| Overload | `response_model` | Returns |
|---|---|---|
| Overload 1 | `None = None` | `SystemOneResponse` |
| Overload 2 | `type[ResponseT]` (required, no default) | `ResponseT` |

`ResponseT` is a `TypeVar` bound to `pydantic.BaseModel` (`_core/schemas/base.py`); it is **not** exported from `typesafe_sdk`. The async version has the same signature and overloads and is `async def`.

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` | "The response body does not match the response model." |

> The docs' "Raises" block for `system_one` lists only the empty-questions and empty-score-criteria cases plus the three HTTP/validation errors; `_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 (now catching `PydanticSerializationError`, `TypeError`, `ValueError`).

### Typed responses with `response_model`

Upstream shows two shapes (`raw/docs/sdk__python__usage.md`). Subclass `SystemOneResponse` to lift named answers onto attributes while keeping `.nouls` / `.choices` / `.scores`, `.request_id` and `.raw_http_response`:

```python
from typesafe_sdk import Noul, NoulAnswer, SystemOneResponse, TypeSafeClient


class BillingResponse(SystemOneResponse):
    billing: NoulAnswer


with TypeSafeClient() as client:
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
        response_model=BillingResponse,
    )
    assert 0 <= result.billing.noul <= 1
    assert result.billing == result.nouls["billing"]
    print(result.request_id)
```

Or define a model from scratch: "It is also possible to define a completely new response model without inheriting from `SystemOneResponse`":

```python
from pydantic import BaseModel

from typesafe_sdk import Noul, NoulAnswer, TypeSafeClient


class BillingAnswers(BaseModel):
    billing: NoulAnswer


class BillingResponse(BaseModel):
    answers: BillingAnswers


result = TypeSafeClient().system_one(
    "I was charged twice.",
    {"billing": Noul(instructions="Is this about billing?")},
    response_model=BillingResponse,
)
assert 0 <= result.answers.billing.noul <= 1
```

Mechanism (`_core/schemas/base.py:parse_response`, `_core/response_types.py`): a `response_model` that inherits the SDK's response base goes through the SDK decoder — unknown answer kinds are dropped, and any field you declared beyond `SystemOneResponse`'s own is lifted out of `answers` into a top-level key before validation. A plain `BaseModel` is validated directly with `model_validate_json(response.content)`, gets **no** `request_id` / `raw_http_response`, and raises `TypeSafeAPIResponseValidationError` (with a dotted `field_path`) when the body does not match. More in [[reference/python-sdk-responses]].

### 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)`. Since 0.7.0 `prepare_system_one` also takes the `response_type` to decode into (`SystemOneResponse` when `response_model is None`). 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()`.

## Pointing the client at another base URL (AI gateways)

"In order to use the SDK with a different API url, set `base_url` on the client or the `TYPESAFE_BASE_URL` environment variable." 0.7.1 added two worked gateway examples to the usage guide; both are reproduced verbatim. Upstream marks each block `skip: next` (they are not executed in the docs test suite).

**OpenRouter** — "Use an OpenRouter API key and an [OpenRouter model ID](https://openrouter.ai/~typesafe/jev-latest/)":

```python
import os

from typesafe_sdk import Noul, TypeSafeClient

with TypeSafeClient(
    api_key=os.environ["OPENROUTER_API_KEY"],
    base_url="https://openrouter.ai/api",
    model="~typesafe/jev-latest",
) as client:
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
    )
    print(result.nouls["billing"].noul)
```

**Vercel AI Gateway** — "[Vercel's TypeSafe-compatible API](https://vercel.com/docs/ai-gateway/sdks-and-apis/typesafe) can be used with the SDK":

```python
import os

from typesafe_sdk import Noul, TypeSafeClient

with TypeSafeClient(
    api_key=os.environ["AI_GATEWAY_API_KEY"],
    base_url="https://ai-gateway.vercel.sh/typesafe",
    model="typesafe-ai/jev",
) as client:
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
    )
    print(result.nouls["billing"].noul)
```

Upstream's one caveat: "This requires the alternative API to follow the [TypeSafe OpenAPI spec](https://api.typesafe.ai/docs/)." Note the gateway key still passes the 0.7.1 API-key validation (printable ASCII, no whitespace), and that neither gateway's pricing or rate limits are covered by [[reference/models-and-pricing]].

## Migrating from 0.6.0

Breaking change in 0.7.0: `msgspec` → `pydantic`, `system_one(..., response_model=)` added, `str` subclasses serialized correctly; `Score.criteria` stays an ordered sequence. The code-level before/after table is in [[reference/python-sdk-changelog]] (v0.7.0, "What the breaking change means in code"); pin `typesafe-sdk>=0.7.1,<0.8`.

## 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 (`model_config = ConfigDict(extra="ignore", ...)`) |

One 0.7.0 wrinkle on the second row: the question `TypedDict`s are now declared `closed=True` (they were `extra_items=JSONValue | None` in 0.6.0), so a dict question carrying an unmodelled key no longer type-checks. It is still sent, and the docs keep the escape hatch with the note: "Unknown fields are a forward-compatibility escape hatch. Ignore their type-checking errors and prefer upgrading the SDK instead." See [[reference/python-sdk-questions]].

```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},
    )
```

## More complete examples

The sync quickstart, with rate-limit and API-error handling added, is the example on [[reference/python-sdk]].

Async, verbatim from the upstream usage guide:

```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. The client-level `model="jev"` is the usage-guide value discussed under [`models` resource](#models-resource) (not a listed model id); here the per-call `model="jev-latest"` overrides it:

```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)
```

## Version notes

- Version documented here: `typesafe-sdk` **0.7.1** (repo captured at commit `0ffd094c72ed9445223060b24ffd7a56aa781fb4`, 2026-09-21; `pyproject.toml` declares `version = "0.7.1"`). See [[reference/python-sdk-changelog]].
- 0.7.0 replaced `msgspec` with `pydantic` and added `response_model` — see [Migrating from 0.6.0](#migrating-from-060).
- 0.6.0 changed `Score.criteria` from an int-keyed dict to an ordered sequence, and that is still the current form — see [[reference/python-sdk-questions]].
- `Usage.billing_units` is gone from the regenerated wire schema; the public `Usage` makes both token counts optional. Details in [[reference/python-sdk-responses]].
- The `sync/client`, `sync/models`, `async/client` and `async/models` doc pages were merged upstream into one page per client (`/sdk/python/api/clients/sync` and `/sdk/python/api/clients/async`); the old URLs redirect.
- 2026-09-22: this page was split out of [[reference/python-sdk]], which now carries only the builder contract.

## Related

- [[reference/python-sdk]] — the builder contract: install, clients, `system_one()`, answers, errors
- [[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, credential redaction
- [[reference/python-sdk-changelog]] — release history and the 0.7.0 migration table
- [[reference/environment-variables]] — `TYPESAFE_*` across SDKs
- [[reference/http-api]] — the wire contract the SDK speaks
- [[reference/models-and-pricing]] — model ids to pass as `model`

## 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.md (https://docs.typesafe.ai/sdk/python/api/clients/sync.md)
- raw/docs/sdk__python__api__clients__async.md (https://docs.typesafe.ai/sdk/python/api/clients/async.md)
- raw/docs/sdk__python__api__constants.md (https://docs.typesafe.ai/sdk/python/api/constants.md)
- raw/docs/sdk__python__changelog.md (https://docs.typesafe.ai/sdk/python/changelog.md)
- raw/github/typesafe-sdk-python/README.md, pyproject.toml, src/typesafe_sdk/__init__.py, src/typesafe_sdk/_core/{config,transport,endpoints,json,logging,constants}.py, src/typesafe_sdk/_core/schemas/base.py (https://github.com/typesafe-ai/typesafe-sdk-python @ 0ffd094c72ed9445223060b24ffd7a56aa781fb4, captured 2026-09-21)
