Shipeasy

Python

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

Production readyOn this page · 5 min readUpdated · July 27, 2026Works with · Python 3.8+ · Django · Flask · FastAPI
Generated from the Python SDK repo's own /docs/ — the same Markdown shipeasy docs get --sdk python overview returns, served raw at https://shipeasy-ai.github.io/sdk-python/pages/overview.md. Edit it in the SDK repo, not here.

shipeasy is the server SDK for Shipeasy — feature flags, remote configs, kill switches, A/B experiments, and metric tracking. It uses your server key and must never be embedded in a browser.

Install

# pip
pip install shipeasy

# poetry
poetry add shipeasy

# uv
uv add shipeasy

Full wiring — frameworks, options, env vars — is in Installation.

Mental model: configure() once, then Client(user) per request

There are exactly two things to learn:

  1. configure() — call it once at process start with your server key and an optional attributes transform (your user object → the Shipeasy attribute map). This is the whole setup story.
  2. shipeasy.Client(user) — construct a cheap, user-bound handle per request and read with no user argument (the user is bound at construction).
import shipeasy

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

# construct once per callsite (cheap; binds the user)
client = shipeasy.Client(current_user)

if client.get_flag("new_checkout"):
    ...
config = client.get_config("billing_copy")
a = client.universe("checkout").assign()  # ≤1 experiment; exposure logs on first get()
if a.get("button_color") == "green":
    ...
client.track("purchase", {"amount": 49})  # on conversion

What the bound Client does

Everything you need per request is on Client(user) — no user argument on any call:

  • get_flag(name, default=False) · get_flag_detail(name)
  • get_config(name, decode=None, default=None)
  • get_killswitch(name, switch_key=None)
  • universe(name).assign()Assignment (.name / .group / .enrolled / .get(field, fallback=None, *, exposure=True)); the first get() on an enrolled assignment logs one exposure (exposure=False peeks without logging)
  • track(event, properties=None)

So an experiment is end-to-end Client-only. Constructing a Client(user) before configure() raises RuntimeError.

The configure family

callwhen
configure(api_key=...)production — your server key
configure_for_testing(...)unit tests — no network, seed overrides
configure_for_offline(...)evaluate real rules from a snapshot / file

After any of them, you read the same way: shipeasy.Client(user).

Feature pages

  • installationpip install shipeasy, frameworks, configure()
  • configurationconfigure(), keys, attributes, one-shot vs poll, options
  • flagsget_flag, get_flag_detail, defaults
  • configsget_config, typed decode, defaults
  • killswitchesget_killswitch
  • error-reportingsee() structured reporting
  • testingconfigure_for_testing, configure_for_offline, overrides
  • openfeatureShipeasyProvider
  • advanced — anon-id middleware, private attrs, sticky bucketing, manual exposure, SSR

The blocks below are the SDK repo's own snippets — the same ones shipeasy docs get --sdk python release/flags returns, with a worked example baked in.

Feature flags

Read a feature flag on a user-bound client. Assumes configure() ran at startup — see Installation.

Basic check

import shipeasy

# construct once per callsite (cheap; binds the user)
client = shipeasy.Client(current_user)

# name                          flag name (required)
# default=False                 returned ONLY when the flag can't be evaluated
#                               (client not ready / flag absent) — never when it
#                               simply resolves off
if client.get_flag("new_checkout", default=False):
    ...

Why it resolved that way — get_flag_detail

import shipeasy

client = shipeasy.Client(current_user)

# returns FlagDetail(value, reason); reason ∈ RULE_MATCH / DEFAULT / OFF /
# OVERRIDE / FLAG_NOT_FOUND / CLIENT_NOT_READY
detail = client.get_flag_detail("new_checkout")
log.info("flag=%s value=%s reason=%s", "new_checkout", detail.value, detail.reason)

React to flag changes (long-running server)

import shipeasy

# requires configure(poll=True); fires after a poll fetches NEW data (200, not 304)
unsubscribe = shipeasy.on_change(lambda: rebuild_local_cache())
# ... later: unsubscribe()

Dynamic configs

Read a dynamic config on a user-bound client. Assumes configure() ran at startup — see Installation.

Raw value

import shipeasy

# construct once per callsite (cheap; binds the user)
client = shipeasy.Client(current_user)

# name                          config name (required)
# default={}                    returned when the key is absent (or decode raises)
config = client.get_config("billing_copy", default={})

Typed decode

import shipeasy

client = shipeasy.Client(current_user)

# decode=lambda v: ...          transform the raw JSON value into the shape you want;
#                               applied on top of overrides — if it raises, default is returned
max_items = client.get_config("billing_copy", decode=lambda v: v["max"], default=0)

Kill switches

Check a kill switch on a user-bound client. Assumes configure() ran at startup — see Installation.

Top-level guard

import shipeasy

# construct once per callsite (cheap; binds the user)
client = shipeasy.Client(current_user)

# name                          kill switch name (required)
if client.get_killswitch("payments"):  # True == engaged (feature killed)
    return fallback()

Named switch — check one configured per-key switch

import shipeasy

client = shipeasy.Client(current_user)

# switch_key                    the variable you check against the switches
#                               CONFIGURED on the kill switch (dashboard "switches");
#                               if that key isn't configured, falls back to the
#                               top-level value above
provider = "stripe"
if client.get_killswitch("payments", provider):
    return use_backup_processor()   # the "stripe" switch is engaged

Track a conversion

Track a metric/conversion event from the bound client. Metrics in the dashboard are computed from these events. Assumes configure() ran at startup — see Installation.

Track an event

import shipeasy

# construct once per callsite (cheap; binds the user)
client = shipeasy.Client(current_user)

# event_name                    the event your metric is built on (required)
# properties={...}              optional payload; numeric/string fields you can
#                               sum/filter on in a metric (private attributes are
#                               stripped before the event leaves the process)
client.track("checkout_started", {"amount": 49, "currency": "usd"})

Fire-and-forget (never blocks your response) and a no-op under configure_for_testing / configure_for_offline. The unit is the bound user (user_id, else anonymous_id); with no unit the call is a no-op.

Track without properties

import shipeasy

client = shipeasy.Client(current_user)

client.track("checkout_started")   # properties are optional
Was this page helpful?
Updated July 26, 2026

On this page