---
title: "Python SDK retries, exceptions, constants"
type: reference
tags: [python, sdk, retries, errors, timeouts, constants]
created: 2026-09-17
updated: 2026-09-17
confidence: high
sources:
  - raw/docs/sdk__python__api__retries.md
  - raw/docs/sdk__python__api__exceptions.md
  - raw/docs/sdk__python__api__constants.md
  - raw/docs/sdk__python__usage.md
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/retry.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/errors.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/constants.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/constants.py
jev_version: "jev-1.13.0"
sdk_python: "0.6.0"
summary: "RetryPolicy fields and defaults, which statuses and exceptions are retried, backoff and Retry-After handling, the full exception hierarchy with attributes, timeouts, and public constants."
---

# Python SDK retries, exceptions, constants

> **TL;DR** Every client retries by default: `RetryPolicy()` = 2 retries, exponential backoff 0.5s → 5.0s with 25% jitter, retryable statuses `{408, 429, 500–599}`, `Retry-After` honored, 30s total retry budget per call. HTTP timeouts are separate and default to 10.0s per operation. Catch `TypeSafeError` for everything, `TypeSafeAPIError` for HTTP failures (`.status`, `.body`, `.headers`, `.endpoint`, `.request_id`), and `TypeSafeAPIConnectionError` / `TypeSafeAPITimeoutError` for transport failures. Disable retries with `RetryPolicy(max_retries=0)`.

## `RetryPolicy`

A frozen `dataclass`, importable as `from typesafe_sdk import RetryPolicy`. It may be passed on the client (`retry=`) or per call (`system_one(..., retry=...)` / `models.list(retry=...)`); a per-call policy fully replaces the client-level one for that call.

```python
RetryPolicy(
    max_retries: int = 2,
    backoff_initial: float = 0.5,
    backoff_max: float = 5.0,
    backoff_jitter: float = 0.25,
    http_statuses: set[int] = {408, 429, *range(500, 600)},
    respect_retry_after: bool = True,
    api_connection_error: bool = True,
    api_timeout_error: bool = True,
    exceptions: set[type[BaseException]] = set(),
    predicate: Callable[[BaseException], bool] | None = None,
    timeout: float | None = 30.0,
)
```

| Field | Type | Default | Description |
|---|---|---|---|
| `max_retries` | `int` | `2` | Maximum retries **after** the initial attempt; `0` disables retries. So the default is up to 3 attempts total. |
| `backoff_initial` | `float` | `0.5` | First backoff delay in seconds, doubled each attempt up to `backoff_max`; zero disables backoff. |
| `backoff_max` | `float` | `5.0` | Maximum backoff delay in seconds; zero disables backoff. |
| `backoff_jitter` | `float` | `0.25` | Fraction of each backoff delay randomly subtracted, between 0 and 1. |
| `http_statuses` | `set[int]` | `{408, 429, 500, 501, …, 599}` (via `default_factory`) | HTTP status codes that are retried. |
| `respect_retry_after` | `bool` | `True` | Honor `Retry-After` and `retry-after-ms` response headers. |
| `api_connection_error` | `bool` | `True` | Retry `TypeSafeAPIConnectionError` (cannot reach or read from the server). |
| `api_timeout_error` | `bool` | `True` | Retry `TypeSafeAPITimeoutError` (request exceeded its timeout). |
| `exceptions` | `set[type[BaseException]]` | `set()` (via `default_factory`) | Additional exception types that trigger a retry, on top of the built-in rules. |
| `predicate` | `Callable[[BaseException], bool] \| None` | `None` | Called with the raised exception; returning `True` triggers a retry in addition to the other rules. |
| `timeout` | `float \| None` | `30.0` | Total retry budget in seconds per SDK call, including the initial attempt and delays; `None` disables the limit. Stops before a retry whose delay would reach or exceed the budget, re-raising the last error. |

### Validation (`__post_init__`)

| Condition | Exception and message |
|---|---|
| `max_retries` not an `int`, or negative | `TypeSafeError("max_retries must be a non-negative integer.")` |
| `backoff_initial` / `backoff_max` non-finite or negative | `TypeSafeError("<name> must be a non-negative, finite number of seconds.")` |
| `backoff_jitter` outside `[0, 1]` | `TypeSafeError("backoff_jitter must be between zero and one.")` |
| `timeout` not `None` and not a positive finite number (or `httpx2.Timeout`) | `TypeSafeError("timeout must be a positive, finite number of seconds.")` (via `resolve_timeout`) |

This validation is the 0.6.0 bug fix "handle invalid values in `RetryPolicy`".

### Which failures are retried

`RetryPolicy._retryable(error)` returns `True` when any of these hold:

1. `isinstance(error, TypeSafeAPITimeoutError)` and `api_timeout_error` is `True`; **else**
2. `isinstance(error, TypeSafeAPIConnectionError)` and `api_connection_error` is `True`; **else**
3. `isinstance(error, TypeSafeAPIError)` and `error.status in http_statuses`; **else** not retryable by the built-in rules — then
4. `isinstance(error, tuple(self.exceptions))`, **or**
5. `predicate is not None and predicate(error)`.

The three built-in branches are mutually exclusive (`TypeSafeAPITimeoutError` is checked first because it subclasses `TypeSafeAPIConnectionError`). Note that `TypeSafeAPIResponseValidationError` subclasses `TypeSafeAPIError` and carries the *successful* status code, so it is retried only if that status happens to be in `http_statuses` — with the defaults, a 200 is not.

Default retryable statuses: `408` (Request Timeout), `429` (Too Many Requests), and every `5xx` from `500` through `599`. Non-retryable by default: `400`, `401`, `403`, `404`, `422`. See [[reference/rate-limits-and-errors]].

### Backoff and `Retry-After`

For attempt *n* (1-based, `n = 1` is the wait before the first retry), `_backoff` computes:

- `exponential = min(backoff_initial * 2**(n-1), backoff_max)`
- `delay = exponential * (1 - random() * backoff_jitter)`, rounded to 3 decimals, capped at `exponential`
- if `backoff_initial == 0` or `backoff_max == 0`, the delay is `0.0`

With defaults that is ~0.5s then ~1.0s (each minus up to 25%).

When `respect_retry_after` is `True` and the error is a `TypeSafeAPIError` carrying a retry header, that header **overrides** the computed backoff entirely. `parse_retry_after` checks headers in this order:

| Header | Unit | Multiplier to ms |
|---|---|---|
| `retry-after-ms` | milliseconds | ×1 |
| `retry-after` | seconds, **or** an HTTP-date | ×1000 |

Rules from `_core/errors.py:parse_retry_after`:

- A numeric value must be finite; negative values on `retry-after-ms` are skipped, a negative `retry-after` returns `None`.
- An empty/whitespace value is treated as `0`.
- A non-numeric `retry-after` is parsed as an HTTP-date; the wait is `max(0, date - now)` in milliseconds.
- The function returns **milliseconds**; the retry layer divides by 1000 to get seconds.

The total budget is enforced with tenacity's `stop_after_attempt(max_retries + 1) | stop_before_delay(timeout)` and `reraise=True`, so the original exception (not a `RetryError`) surfaces.

Each retried request carries `X-TypeSafe-Retry-Count: <attempt>` and logs `"<METHOD> <url> retry <n>"` at INFO.

### Examples

```python
from typesafe_sdk import RetryPolicy, TypeSafeClient

client = TypeSafeClient(
    retry=RetryPolicy(max_retries=3, timeout=10.0, http_statuses={429, 500, 502, 503, 504})
)
```

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

with TypeSafeClient() as client:
    # client-level
    client_policy = RetryPolicy(max_retries=3, backoff_max=0.2, timeout=1.0)

    # per-call override
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
        retry=RetryPolicy(max_retries=0),  # no retries for this call
    )
    print(result.nouls["billing"].noul)
```

Retry on an extra exception type, or with a custom predicate:

```python
import httpx2

from typesafe_sdk import RetryPolicy, TypeSafeAPIError, TypeSafeClient

policy = RetryPolicy(
    exceptions={httpx2.RemoteProtocolError},
    predicate=lambda error: isinstance(error, TypeSafeAPIError) and error.status == 409,
)
client = TypeSafeClient(retry=policy)
```

## Timeouts

Two independent budgets:

| Setting | Default | Scope | Where |
|---|---|---|---|
| `timeout` on the client / per call | `10.0` s (`constants.DEFAULT_TIMEOUT`) | each HTTP operation (connect/read/write/pool, per `httpx2.Timeout` semantics) | `TypeSafeClient(timeout=...)`, `system_one(..., timeout=...)`, `models.list(timeout=...)` |
| `RetryPolicy.timeout` | `30.0` s | the whole SDK call: initial attempt + all retries + delays | `RetryPolicy(timeout=...)` |

Precedence for the HTTP timeout: per-call `timeout` → client `timeout` → `http_client.timeout` (when you supplied an `http_client` and passed no `timeout`) → `DEFAULT_TIMEOUT` (10.0).

Pass an `httpx2.Timeout` for fine-grained control:

```python
import httpx2

from typesafe_sdk import TypeSafeClient

client = TypeSafeClient(timeout=httpx2.Timeout(connect=2.0, read=20.0, write=10.0, pool=5.0))
```

An expired HTTP timeout raises `TypeSafeAPITimeoutError`, which is retryable by default; when the retry budget is exhausted it propagates.

## Exception hierarchy

```
Exception
└── TypeSafeError
    ├── TypeSafeAPIError
    │   ├── TypeSafeBadRequestError            (400)
    │   ├── TypeSafeAuthenticationError        (401)
    │   ├── TypeSafePermissionDeniedError      (403)
    │   ├── TypeSafeNotFoundError              (404)
    │   ├── TypeSafeUnprocessableEntityError   (422)
    │   ├── TypeSafeRateLimitError             (429)
    │   ├── TypeSafeInternalServerError        (5xx)
    │   └── TypeSafeAPIResponseValidationError (2xx with an unusable body)
    └── TypeSafeAPIConnectionError   (also subclasses ConnectionError)
        └── TypeSafeAPITimeoutError  (also subclasses TimeoutError)
```

| Class | Bases | Raised when | Extra attributes |
|---|---|---|---|
| `TypeSafeError` | `Exception` | Base for all SDK failures; also raised directly for config/validation problems (missing API key, invalid timeout, empty questions, invalid `RetryPolicy`, unencodable body, missing request id). | — |
| `TypeSafeAPIError` | `TypeSafeError` | Any unsuccessful HTTP response after retries. | `status: int`, `body: Any`, `headers: httpx2.Headers`, `endpoint: str \| None`, `request_id: str \| None` (property) |
| `TypeSafeBadRequestError` | `TypeSafeAPIError` | 400 — the request was invalid. | inherited |
| `TypeSafeAuthenticationError` | `TypeSafeAPIError` | 401 — authentication failed. | inherited |
| `TypeSafePermissionDeniedError` | `TypeSafeAPIError` | 403 — access was denied. | inherited |
| `TypeSafeNotFoundError` | `TypeSafeAPIError` | 404 — resource not found. | inherited |
| `TypeSafeUnprocessableEntityError` | `TypeSafeAPIError` | 422 — failed server validation. | inherited |
| `TypeSafeRateLimitError` | `TypeSafeAPIError` | 429 — rate limit exceeded. | `retry_after_ms: float \| None` plus inherited |
| `TypeSafeInternalServerError` | `TypeSafeAPIError` | any status ≥ 500. | inherited |
| `TypeSafeAPIResponseValidationError` | `TypeSafeAPIError` | A successful HTTP response whose body was missing or structurally invalid required data. | `field_path: str` (e.g. `answers.tone.confidence`); `args == (status, body, headers, field_path, endpoint)` |
| `TypeSafeAPIConnectionError` | `TypeSafeError`, `ConnectionError` | The request failed without an HTTP response. | — |
| `TypeSafeAPITimeoutError` | `TypeSafeAPIConnectionError`, `TimeoutError` | The request exceeded its configured timeout. | `timeout: float \| httpx2.Timeout` |

Status→class mapping (`STATUS_ERROR_TYPES` in `_core/errors.py`): 400, 401, 403, 404, 422, 429 map to the classes above; any other status ≥ 500 becomes `TypeSafeInternalServerError`; anything else becomes a plain `TypeSafeAPIError`.

Because `TypeSafeAPIConnectionError` also subclasses `ConnectionError` and `TypeSafeAPITimeoutError` also subclasses `TimeoutError`, generic `except ConnectionError` / `except TimeoutError` handlers in surrounding code will catch them.

### Error attributes in detail

- **`status`** — HTTP response status code.
- **`body`** — the server's JSON error body, plain response text, or `None` for an empty body.
- **`headers`** — `httpx2.Headers` of the response.
- **`endpoint`** — the request method and URL, without credentials, query parameters, or fragment, when available (e.g. `POST https://api.typesafe.ai/v1/systemone`). This is the 0.6.0 "error messages include http details and metadata" improvement.
- **`request_id`** — the `x-typesafe-request-id` response header, or `None` if absent.
- **`retry_after_ms`** (429 only) — the server's requested wait in **milliseconds**, or `None`.
- **`field_path`** (validation only) — dotted path to the first offending field.

`__str__` renders `"{endpoint}: {status} {message} (request_id={id})"`, omitting the parts that are unavailable. The message is derived from the body by `extract_message`, which tries, in order: `body` as a string → `body["error"]` (string) → `body["error"]["message"]` → `body["message"]` → `body["detail"]` (string) → `body["detail"]["message"]` → a FastAPI-style `body["detail"]` list joined as `"loc.path: msg; …"`. If nothing matches, the serialized body is truncated to `MAX_ERROR_BODY_LENGTH` (200) characters with an ellipsis; an empty body yields `"status code (no body)"`. `__repr__` deliberately omits the body and headers. Exceptions are picklable as of 0.6.0.

### Handling examples

```python
from typesafe_sdk import TypeSafeAPIError, TypeSafeClient

try:
    client.system_one(state, questions)
except TypeSafeAPIError as error:
    print(error.status, error.request_id)
```

Full triage:

```python
import logging
import time

from typesafe_sdk import (
    Noul,
    TypeSafeAPIConnectionError,
    TypeSafeAPIError,
    TypeSafeAPIResponseValidationError,
    TypeSafeAPITimeoutError,
    TypeSafeAuthenticationError,
    TypeSafeClient,
    TypeSafeError,
    TypeSafeRateLimitError,
)

with TypeSafeClient() as client:
    try:
        result = client.system_one(
            "I was charged twice.",
            {"billing": Noul(instructions="Is this about billing?")},
        )
    except TypeSafeAuthenticationError:
        raise SystemExit("Check TYPESAFE_API_KEY")
    except TypeSafeRateLimitError as error:
        wait = (error.retry_after_ms or 1000) / 1000
        logging.warning("rate limited, waiting %.1fs (request %s)", wait, error.request_id)
        time.sleep(wait)
    except TypeSafeAPIResponseValidationError as error:
        logging.error("bad response body at %s: %s", error.field_path, error.endpoint)
    except TypeSafeAPITimeoutError as error:
        logging.error("timed out after %s", error.timeout)
    except TypeSafeAPIConnectionError as error:
        logging.error("connection failure: %s", error)
    except TypeSafeAPIError as error:
        logging.error("%s %s", error.status, error)
    except TypeSafeError as error:  # config / validation problems, no HTTP made
        logging.error("client-side error: %s", error)
    else:
        print(result.nouls["billing"].noul)
```

Catch order matters: `TypeSafeAPITimeoutError` before `TypeSafeAPIConnectionError`, the specific `TypeSafeAPIError` subclasses before `TypeSafeAPIError`, and `TypeSafeError` last.

## Constants

### Public — `typesafe_sdk.constants`

| Name | Value | Meaning |
|---|---|---|
| `API_KEY_ENV` | `'TYPESAFE_API_KEY'` | Environment variable for the API key. |
| `BASE_URL_ENV` | `'TYPESAFE_BASE_URL'` | Environment variable for the API base URL. |
| `DEFAULT_MODEL_ENV` | `'TYPESAFE_DEFAULT_MODEL'` | Environment variable for the default model. |
| `LOG_LEVEL_ENV` | `'TYPESAFE_LOG_LEVEL'` | Environment variable for the logging level. |
| `DEFAULT_BASE_URL` | `'https://api.typesafe.ai'` | Default API base URL. |
| `DEFAULT_MODEL` | `'jev-latest'` | Default model name. |
| `DEFAULT_TIMEOUT` | `10.0` | Default timeout in seconds for each HTTP operation. |

```python
from typesafe_sdk import constants

print(constants.DEFAULT_BASE_URL, constants.DEFAULT_MODEL, constants.DEFAULT_TIMEOUT)
```

### Internal — `typesafe_sdk._core.constants`

Not part of the public API (absent from the docs and from `__all__`), but they define the wire behavior you will see on the network:

| Name | Value |
|---|---|
| `SYSTEM_ONE_PATH` | `/v1/systemone` |
| `MODELS_PATH` | `/v1/models` |
| `SDK_NAME` | `typesafe-sdk` |
| `LOGGER_NAME` | `typesafe_sdk` |
| `JSON_CONTENT_TYPE` | `application/json` |
| `MAX_ERROR_BODY_LENGTH` | `200` |
| `AUTHORIZATION_HEADER` | `Authorization` |
| `ACCEPT_HEADER` | `Accept` |
| `CONTENT_TYPE_HEADER` | `Content-Type` |
| `USER_AGENT_HEADER` | `User-Agent` |
| `SDK_HEADER` | `X-TypeSafe-SDK` |
| `RUNTIME_HEADER` | `X-TypeSafe-Runtime` |
| `RETRY_COUNT_HEADER` | `X-TypeSafe-Retry-Count` |
| `REQUEST_ID_HEADER` | `x-typesafe-request-id` |
| `RETRY_AFTER_HEADER` | `retry-after` |
| `RETRY_AFTER_MS_HEADER` | `retry-after-ms` |
| `SECRET_HEADERS` | `frozenset({authorization, proxy-authorization, x-api-key, api-key, cookie, set-cookie})` |

Treat these as version-specific implementation detail; depend on the public `constants` module instead.

## Version notes

0.6.0 changed retry/error behavior in two ways recorded in the changelog: `RetryPolicy` now rejects invalid values (see the validation table), and errors carry HTTP details and metadata (`endpoint`, richer `__str__`). Exceptions and responses became picklable. See [[reference/python-sdk-changelog]].

## Related

- [[reference/python-sdk]] — clients, `system_one()`, headers, logging
- [[reference/python-sdk-responses]] — what you get when no exception is raised
- [[reference/python-sdk-changelog]] — release history
- [[reference/rate-limits-and-errors]] — HTTP status codes and retry semantics at the API level
- [[reference/environment-variables]] — `TYPESAFE_*` across SDKs
- [[reference/javascript-sdk-errors]] — the JS/TS equivalents
- [[reference/http-api]] — endpoints and error bodies

## Sources

- raw/docs/sdk__python__api__retries.md (https://docs.typesafe.ai/sdk/python/api/retries.md)
- raw/docs/sdk__python__api__exceptions.md (https://docs.typesafe.ai/sdk/python/api/exceptions.md)
- raw/docs/sdk__python__api__constants.md (https://docs.typesafe.ai/sdk/python/api/constants.md)
- raw/docs/sdk__python__usage.md (https://docs.typesafe.ai/sdk/python/usage.md)
- raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/retry.py, _core/errors.py, _core/constants.py, _core/transport.py, _core/config.py, constants.py (https://github.com/typesafe-ai/typesafe-sdk-python @ 420ef4ffb612d5a539a1e0f0fe883ff6770340af)
