Shipeasy

Python

The Shipeasy Python server SDK — local evaluation, configs, experiments, WSGI/ASGI anon middleware, and metric tracking.

Production readyOn this page · 4 min readUpdated · June 18, 2026Works with · Python 3.9+ · Flask · Django · FastAPI

Server-side flags, configs, experiments, and tracking for Python. Server-key only — never embed in browsers. Evaluates locally against a background-polled blob — see the shared evaluation model.

Install

pip install shipeasy

Configure

Initialise once with the server key, plus an optional attributes transform that maps your user object onto the Shipeasy attribute map. configure() starts a background poll.

import shipeasy

shipeasy.configure(
    api_key=os.environ["SHIPEASY_SERVER_KEY"],
    attributes=lambda u: {"user_id": u.id, "plan": u.plan, "country": u.country},
)

Bind a client to the user

Construct a lightweight Client from your user object — the attributes transform turns it into the evaluation context, so the getters take no user argument:

flags = shipeasy.Client(current_user)

Evaluate a feature flag

if flags.get_flag("new_checkout"):
    ...

A default is returned only when the value can't be evaluated (client not ready or gate absent) — a gate that evaluates False returns False:

flags.get_flag("new_checkout", default=True)

For the value plus the reason, use get_flag_detail — see Evaluation reasons:

d = flags.get_flag_detail("new_checkout")
print(d.value, d.reason)  # e.g. True RULE_MATCH

Read a dynamic config

config = flags.get_config("billing_copy", default={"title": "Welcome"})

Resolve an experiment

result = flags.get_experiment("checkout_button", default_params={"color": "blue"})
if result.in_experiment:
    print(result.group, result.params)

Track an event

flags.track("purchase", {"amount": 49})

The low-level Engine

The per-call style still works for back-compat. Construct shipeasy.Engine directly and pass the user on every call — use init_once() for serverless (one synchronous fetch, no thread):

engine = shipeasy.Engine(api_key=os.environ["SHIPEASY_SERVER_KEY"])
engine.init_once()  # one fetch, no background thread
engine.get_flag("new_checkout", {"user_id": "u_123", "country": "US"})

Anonymous visitors

Mount the middleware once; logged-out requests then bucket on the shared __se_anon_id cookie automatically — no per-call wiring:

# WSGI (Flask, Django)
from shipeasy.middleware import AnonIdMiddleware
app.wsgi_app = AnonIdMiddleware(app.wsgi_app)

# ASGI (FastAPI, Starlette)
from shipeasy.middleware import AnonIdASGIMiddleware
app.add_middleware(AnonIdASGIMiddleware)

shipeasy.Client(anon_user).get_flag("new_checkout")  # buckets on the cookie

Testing

Engine.for_testing() does zero network — no api*key, init/track are no-ops. Seed with override*\*and reset withclear_overrides. See Testing.

engine = shipeasy.Engine.for_testing()
engine.override_flag("new_checkout", True)
assert engine.get_flag("new_checkout", {"user_id": "u_123"}) is True
engine.clear_overrides()

Errors & feedback

Report a handled exception with see() so it folds into the errors primitive with a one-sentence consequence — what feature broke and how it degraded. see() rides the same configure() boot (server key); there is no separate error SDK or second key.

try:
    charge_card(order, prices)
except Exception as e:
    shipeasy.see(e).causes_the("checkout").to("use cached prices").extras({"order_id": order.id})

Use shipeasy.see_violation("large query") for a non-exception problem, and shipeasy.control_flow_exception(e).because("…") for expected control flow that should report nothing. Full grammar — consequence phrasing, control-flow exceptions, anti-patterns — is in Error reporting with see().

The in-app bug & feature report overlay is a standalone <script> tag you drop into your frontend — platform-agnostic, no server SDK required. See The devtools overlay.

Was this page helpful?
✎ Edit this page

On this page