SDKs · Python
Python SDK
A server SDK for Python services. It downloads your environment's ruleset on a background thread and evaluates in-process, so a flag check is a dictionary lookup and a hash, not an HTTP call.
httpx)ds_srv_ keys onlyThe Python SDK is a server SDK. Do not embed a ds_srv_ key in a distributed application (a desktop app, a notebook you share). For untrusted clients use the JavaScript client SDK or the evaluate endpoint with a client key.
Install#
$ pip install darkship
$ poetry add darkship
$ uv add darkship
$ pip install "darkship[openfeature]" # adds the OpenFeature providerInitialise#
import os
import darkship
ds = darkship.init(
sdk_key=os.environ["DARKSHIP_SDK_KEY"], # ds_srv_prod_...
) # host: DARKSHIP_BASE_URL or base_url=...
ds.on("error", lambda err: log.warning("darkship: %s", err))
ready = ds.wait_for_ready(timeout=3.0) # True or False, never raisesCreate one client per process and share it. It owns a polling thread and an in-memory ruleset; creating one per request would multiply both.
Init options#
| Argument | Type | Default | Description |
|---|---|---|---|
sdk_key | str | required | A ds_srv_ key. Other prefixes raise ValueError at init. |
base_url | str | DARKSHIP_BASE_URL, else https://sdk.darkship.tech | Evaluation host. Must be https (plain http only for loopback, unless allow_insecure_http=True). |
allow_insecure_http | bool | False | Allow plain http on a non-loopback host (private network, test double). Logs a warning on the darkship logger: the key travels unencrypted. |
streaming | bool | True | SSE (GET /v1/sdk/stream): a change reaches the SDK within seconds. Any stream error falls back to polling, which never stops, and the stream reconnects. False for polling only. |
poll_interval | float (s) | 30.0 | The fallback poll (at least 30 s while the stream is connected). Jittered by up to 20%, minimum 1.0. Retry-After is honoured and failures back off exponentially. |
timeout | float (s) | 3.0 | Bound on every network call. |
bootstrap | dict | None | A snapshot saved earlier (ds.snapshot()), used until the first fetch succeeds. |
offline | bool | False | Never open a socket; serve bootstrap only. For tests and air-gapped jobs. |
transport | httpx.BaseTransport | None | Inject a transport (proxies, mTLS, httpx.MockTransport in tests). |
telemetry | bool | dict | True | {"flush_interval": 60.0} to tune; False to turn off counters. |
Frameworks and process models#
Background threads don't survive fork(). With Gunicorn or uWSGI preload, create the client in a post_fork hook (or lazily on first use), not at import time in the master process.
from contextlib import asynccontextmanager
from fastapi import FastAPI
import darkship, os
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.ds = darkship.init(sdk_key=os.environ["DARKSHIP_SDK_KEY"])
app.state.ds.wait_for_ready(timeout=3.0)
yield
app.state.ds.close(timeout=2.0) # flushes counters
app = FastAPI(lifespan=lifespan)from django.apps import AppConfig
import darkship, os
class CoreConfig(AppConfig):
name = "core"
ds = None
def ready(self): # runs once per worker process
CoreConfig.ds = darkship.init(sdk_key=os.environ["DARKSHIP_SDK_KEY"])import darkship
with darkship.init(sdk_key=KEY) as ds: # close() on exit
ds.wait_for_ready(timeout=3.0)
if ds.bool_variation("nightly-reindex", {"key": "job"}, False):
reindex()Evaluate flags#
ctx = {"key": "usr_marta", "plan": "pro", "country": "BR", "orgId": "org_acme"}
on = ds.bool_variation("new-checkout-flow", ctx, False)
banner = ds.string_variation("promo-banner-text", ctx, "Welcome back")
retries = ds.int_variation("max-retries", ctx, 3)
ratio = ds.float_variation("sample-ratio", ctx, 0.1)
limits = ds.json_variation("rate-limits", ctx, {"rps": 10, "burst": 20})
cutover = ds.date_variation("cutover-at", ctx, None) # aware datetime in UTC
d = ds.get_boolean_details("new-checkout-flow", ctx, False)
# EvaluationDetail(value=True, variation='on', reason='FALLTHROUGH', error_code=None, bucket=4127)| Method | Alias | Returns | Flag type |
|---|---|---|---|
get_boolean_value(key, ctx, default) | bool_variation | bool | boolean |
get_string_value(...) | string_variation | str | string |
get_int_value(...) | int_variation | int | int |
get_float_value(...) | float_variation | float | float |
get_json_value(...) | json_variation | Any (dict, list, ...) | json |
get_date_value(...) | date_variation | datetime (UTC) | date |
get_*_details(...) | *_variation_detail | EvaluationDetail | same as above |
Also: all_flags(ctx) returns {flag_key: EvaluationDetail}.
Reasons are OFF, TARGET_MATCH, RULE_MATCH, FALLTHROUGH, NOT_READY and ERROR; error codes are FLAG_NOT_FOUND, TYPE_MISMATCH, PROVIDER_NOT_READY, MALFORMED_FLAG, INVALID_CONTEXT and GENERAL. A percentage rollout adds bucket (0 to 9999) to the detail; there is no rollout reason. See the reason table. The evaluation is the same algorithm as in JavaScript and on the evaluation service, verified against shared conformance fixtures. A user_id bucketed in Python lands in the same bucket as in Node.
Context#
A dict with a required "key" (the user id) and optional "email", "name" and "groups" (a list of strings). Target lists match by key, email (normalised, case-insensitive) or groups (any element). Values may be str, int, float, bool or lists of strings. Missing attributes never match positive operators and nothing raises. Never put secrets in a context; in server mode it stays in your process.
ctx = {
"key": "usr_marta",
"email": "Marta@Acme.com", # matches ana@acme.com style lists, any casing
"groups": ["beta-testers", "acme-staff"],
"plan": "pro",
}
on = ds.bool_variation("ai-search-beta", ctx, False)Events#
ds.on("ready", lambda e: log.info("ruleset %s", e["version"]))
ds.on("update", lambda e: log.info("changed: %s", e["changed"]))
ds.on("error", lambda e: log.warning("%s: %s", e.code, e))Handlers run on the SDK's thread, so keep them short and thread-safe.
Telemetry and privacy#
Identical to the other SDKs: per flag × variation × one-minute counters, flushed on an interval and at close(); no context and no user-level data. The field list is in the JavaScript SDK page. The sdk field reads python/<version>.
Errors and defaults#
Evaluation methods do not raise. A missing flag, a type mismatch or a not-ready client all return the default. Programmer errors at init (ValueError for a missing or non-server key) are the only exceptions. wait_for_ready returns False on timeout. If the network drops after a ruleset was loaded, evaluation continues from it and an error event is emitted on each failed refresh, with exponential backoff and jitter.
OpenFeature#
Install the extra and register the provider. It wraps the native client (pass your own with DarkshipProvider(ds)), so the two paths cannot diverge.
from openfeature import api
from openfeature.evaluation_context import EvaluationContext
from darkship.openfeature import DarkshipProvider
api.set_provider_and_wait(DarkshipProvider(sdk_key=os.environ["DARKSHIP_SDK_KEY"]))
client = api.get_client()
client.get_boolean_value("new-checkout-flow", False, EvaluationContext("usr_marta", {"plan": "pro"}))targeting_key is the darkship key; other attributes are targeting attributes. Reasons map to STATIC (off), TARGETING_MATCH, SPLIT (a percentage rollout decided), DEFAULT (fallthrough) and ERROR. Date flags resolve as an ISO-8601 string through resolve_object_details. Provider events: READY, CONFIGURATION_CHANGED, ERROR.
Shutdown#
import atexit
atexit.register(lambda: ds.close(timeout=2.0)) # stops the thread, flushes counters (best effort)Testing your code#
Unit tests shouldn't hit the network. Pass a bootstrap ruleset and disable the poller and telemetry:
import pytest, darkship
@pytest.fixture
def ds():
client = darkship.init(
sdk_key="ds_srv_test_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
bootstrap=FIXTURE_RULESET, offline=True, telemetry=False,
)
yield client
client.close()offline=TrueOffline mode never opens a socket: it serves the bootstrap ruleset and nothing else. Evaluation behaves exactly as in production, including bucketing.
Versions#
The package is at 0.1.0. Until 1.0, minor versions may include breaking changes, listed in the changelog. Python 3.10 or newer; the one runtime dependency is httpx. The evaluator is verified against the shared language-neutral conformance vectors.
Thanks. This helps us prioritise the docs.