Shipeasy
SDKsReferencePython

Agent skill

Installable LLM skill for the Python SDK (configure() + Client(user), evaluate, experiment + track, testing).

An installable agent skill for the Python SDK. Fetch it with shipeasy docs skill --sdk python (--install writes it to your agent skills dir), or copy it below — the YAML frontmatter installs with it.

---
name: shipeasy-python
description: Use Shipeasy (feature flags, configs, kill switches, A/B experiments, i18n) from Python. Covers configure() + Client(user), get_flag/get_config/universe(name).assign(), track, testing, OpenFeature.
---

# Shipeasy Python SDK

Server SDK (`pip install shipeasy`, `import shipeasy`). Server-key only — never
embed in a browser. Requires Python 3.9+.

Two things only: **`configure()`** once at startup, then **`shipeasy.Client(user)`**
per request.

> **Pulling deeper docs.** Each section below links its full reference page and
> copy-paste snippets — fetch any of them as raw Markdown when you need more than
> this summary. Discover the whole tree from the manifest:
> `https://shipeasy-ai.github.io/sdk-python/manifest.json` (lists every
> `pages/<key>.md` and `snippets/<group>/<leaf>.md`). All URLs below are
> `https://shipeasy-ai.github.io/sdk-python/…`.

## Configure once

```python
import shipeasy

shipeasy.configure(
    api_key="sdk_server_...",
    attributes=lambda u: {"user_id": u.id, "country": u.country, "plan": u.plan},
)
```

Omit `attributes` if your user object is already the attribute map. For a
long-running server pass `poll=True` to keep the blob fresh in the background.

**Quiet outside production (0.17.0+).** By default the SDK makes NO outbound
request (no fetch, `track`, exposure, `see()`, or telemetry) unless it detects a
production environment — via `SHIPEASY_ENV`/`APP_ENV`/`ENV`/`PYTHON_ENV`
(`production`/`prod`), else the `env` option (default `"prod"`). On a dev machine
or in CI, set `SHIPEASY_ENV=production` or pass `is_network_enabled=True` to turn
egress back on; pass `is_network_enabled=False` to force fully offline.

**Django:** add `"shipeasy.django"` to `INSTALLED_APPS`, run
`python manage.py shipeasy_install` (idempotently wires the anon-id middleware +
a `SHIPEASY = {...}` settings block + `.env`), then set `SHIPEASY["SERVER_KEY"]`.
The app's AppConfig calls `configure()` from that dict at boot — no manual
`configure()` call needed.

→ More: `pages/installation.md` (per-framework setup, incl. the Django app),
`pages/configuration.md` (every option).

## Evaluate (bound `Client(user)`)

Bind the user once per request, then call without re-passing it — `assign` and
`track` are on the bound client too, so experiments are end-to-end here:

```python
client = shipeasy.Client(current_user)

client.get_flag("new_checkout")                 # bool; default= only on un-evaluable
client.get_config("billing_copy", default={})   # typed JSON; default= on absent
client.get_killswitch("payments_breaker")       # bool kill switch

# A universe is a mutual-exclusion pool: a unit lands in <=1 experiment.
# assign() is side-effect free; the first a.get() on an enrolled assignment
# logs one exposure (deduped). a.get(field, fallback, *, exposure=False) peeks
# without logging. It resolves variant override -> universe default -> fallback
# (works even when not enrolled).
a = client.universe("checkout").assign()
a.name, a.group, a.enrolled                     # None/None/False when not enrolled
a.get("color", "blue")                          # first read logs the exposure

client.track("purchase", {"amount": 49})        # conversion / metric event
```

`get_flag_detail` returns `FlagDetail(value, reason)` (reasons: `RULE_MATCH`,
`DEFAULT`, `OFF`, `OVERRIDE`, `FLAG_NOT_FOUND`, `CLIENT_NOT_READY`).

→ More: pages `pages/flags.md` · `pages/configs.md` · `pages/killswitches.md`
(incl. named switches) · `pages/experiments.md`. Snippets
`snippets/release/{flags,configs,killswitches,experiments}.md` and
`snippets/metrics/track.md` (event tracking).

## Testing (no network)

Use the `configure()` siblings — seed overrides, read through the same `Client`:

```python
shipeasy.configure_for_testing(
    flags={"new_checkout": True},
    configs={"billing_copy": {"title": "Welcome"}},
)
assert shipeasy.Client({"user_id": "u_123"}).get_flag("new_checkout") is True

# flip a value on the spot, mid-test:
shipeasy.override_flag("new_checkout", False)
shipeasy.clear_overrides()
```

An `experiments={name: (group, params)}` override is a **pure override** — it wins
over blob eval but only surfaces through `universe(name).assign()` for an
experiment already in the loaded blob. Pair it with a real experiment blob offline:

```python
shipeasy.configure_for_offline(
    snapshot={"flags": {"gates": {}, "configs": {}},
              "experiments": {"experiments": {"exp": {"universe": "u", "status": "running",
                              "salt": "s", "allocationPct": 10000,
                              "groups": [{"name": "control", "weight": 10000, "params": {}}]}},
                              "universes": {"u": {}}}},
    experiments={"exp": ("treatment", {"color": "green"})},   # override wins over the variant
)
assert shipeasy.Client({"user_id": "u_123"}).universe("u").assign().group == "treatment"
```

Offline (real rules from a snapshot / file):

```python
shipeasy.configure_for_offline(path="snapshot.json")
# or snapshot={"flags": {...}, "experiments": {...}}, plus optional overrides
```

→ More: `pages/testing.md` (override helpers + a working example
`shipeasy-snapshot.json`).

## OpenFeature

```python
import shipeasy                                   # pip install "shipeasy[openfeature]"
from openfeature import api
from shipeasy.openfeature import ShipeasyProvider

shipeasy.configure(api_key="sdk_server_...", poll=True)
api.set_provider(ShipeasyProvider())             # uses the configured global
```

Boolean → gate; string/int/float/object → config.

→ More: `pages/openfeature.md` (reason mapping, type routing).

## Error reporting — see()

```python
from shipeasy import see
try:
    charge(order)
except PaymentError as e:
    see(e).causes_the("checkout").to("use the backup processor")
    # extras inline on the terminal — NEVER .causes_the(x).extras({...}).to(y),
    # which splits the consequence sentence in half:
    see(e).causes_the("checkout").to("use cached prices", {"order_id": oid})
```

Buffer request-wide context from any layer with `shipeasy.add_extras(order_id=...)`
— every later `see()` in the same request merges it in (ContextVar-scoped; the
WSGI/ASGI/Django middleware clears it per request; `shipeasy.clear_extras()`
outside a request). A stray `.extras` after `.to` is ignored (never raises) — the
extras are dropped, so use the inline form or `add_extras`.

→ More: `pages/error-reporting.md` · snippets `snippets/ops/see.md`
(`.extras()`, `add_extras`, violations, control-flow exceptions).

## Other surfaces

- Anon bucketing: `AnonIdMiddleware` (WSGI) / `AnonIdASGIMiddleware` (ASGI) mint
  the shared `__se_anon_id` cookie; anonymous `get_flag` then just works.
- `configure(private_attributes=[...])` strips keys from outbound events.
- `configure(sticky_store=InMemoryStickyStore())` pins experiment assignment.
- Exposure fires on read: the first `a.get(field)` on an enrolled assignment
  logs one exposure (deduped per process and durably per unit/experiment/group);
  `assign()` itself is side-effect free, and `a.get(field, fallback,
  exposure=False)` peeks without logging. There is no manual `log_exposure`.
- SSR tags — every argument optional (defaults from `configure()`):
  `shipeasy.bootstrap_script_tag(user)`, `shipeasy.i18n_script_tag()`,
  `shipeasy.devtools_script_tag()` (overlay; needs `project_id` + `client_key`;
  opens with Shift+Alt+S or `?se=1`).

→ More: `pages/advanced.md`.

## i18n

No server-side `t()`. The browser's Shipeasy **client** SDK renders labels; this
server SDK only emits the loader tag (public client key) during SSR.

→ More: `pages/i18n.md` · snippets `snippets/i18n/setup.md`.
Was this page helpful?
Updated July 26, 2026