Python SDK: install, clients, system_one()
TL;DR
pip install typesafe-sdk(import nametypesafe_sdk), setTYPESAFE_API_KEY, thenwith 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 aSystemOneResponse. The async twin isAsyncTypeSafeClientwithawait client.system_one(...)andaclose().
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-aion PyPI (version 0.1.0) is a redirect shim that simply depends ontypesafe-sdk. Installing it works, but you stillimport typesafe_sdk. Prefertypesafe-sdkdirectly.typesafe0.9.1 on PyPI is an unrelated third-party package and is not TypeSafe AI.- The cookbook pages use a
pypi.typesafe.aiindex for acooksafehelper; that host returned 404 publicly as of 2026-09-17.
Then set the API key (create one at https://console.typesafe.ai/):
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.
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,
)
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_urlisrstrip("/")-ed.timeoutis validated byresolve_timeout: a non-httpx2.Timeoutvalue must be finite and> 0.- The resolved
api_keyis stored on a dataclass field withrepr=False, as are the default headers, so it does not leak throughrepr().
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()
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 Python SDK responses, answers, usage, models.
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_onelists only the empty-questions and empty-score-criteria cases;_core/questions.pyadditionally raisesTypeSafeErrorfor a malformed question dictionary and for achoice/scoredict with no"criteria"key, and_core/transport.pyraises 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:
{"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 HTTP API: POST /v1/systemone and GET /v1/models 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 TYPESAFE_* environment variables across SDKs and Python SDK retries, exceptions, constants for the constants module in full.
models resource
client.models is a cached property returning Models (sync) or AsyncModels (async). It has one method:
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 Python SDK responses, answers, usage, models and Models, aliases, pricing, rate limits, context.
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:
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 Models, aliases, pricing, rate limits, context (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.
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) |
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:
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:
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:
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
TypeSafeClientfor scripts, sync web frameworks, and notebooks; useAsyncTypeSafeClientinside asyncio services and when fanning out many calls concurrently (see Speculative fan-out). - Do not create a client per request: construction resolves config and builds an
httpx2client. Build one per process and reuse it. - If you only need one call in one language-agnostic place, the raw HTTP API: POST /v1/systemone and GET /v1/models is equivalent; the SDK adds typed questions/answers and the default retry policy.
Version notes
- Version documented here:
typesafe-sdk0.6.0 (repo commit420ef4ffb612d5a539a1e0f0fe883ff6770340af, 2026-09-15). See Python SDK changelog. - 0.6.0 changed
Score.criteriafrom an int-keyed dict to an ordered sequence — see Python SDK question types (Noul, Choice, Score). Usage.billing_unitsexists in the generated wire schema but not on the publicUsagetype; details in Python SDK responses, answers, usage, models.
Related
- Python SDK question types (Noul, Choice, Score) —
Noul,Choice,Scoreand their dict forms - Python SDK responses, answers, usage, models —
SystemOneResponse, answers, usage, models - Python SDK retries, exceptions, constants —
RetryPolicy, exceptions, constants - Python SDK changelog — release history
- HTTP API: POST /v1/systemone and GET /v1/models — the wire contract the SDK speaks
- TYPESAFE_* environment variables across SDKs —
TYPESAFE_*across SDKs - JavaScript/TypeScript SDK: install, client, choice/score/noul — the JS/TS equivalent
- system-one-adapter: LLM-backed drop-in for TypeSafeClient — LLM-backed drop-in for
TypeSafeClient - Quickstart: first call in HTTP, Python, JS — first call in HTTP, Python, JS
- State: what you send Jev — what
statemay contain - Primitives: Choice, Score, Noul — 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-aishim, unrelatedtypesafe) collected 2026-09-17 and recorded in CLAUDE.md