Skip to main content
darkship docs
Open console

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.

Updated Oct 28, 20265 min read Edit this page on GitHub (opens in a new tab)

darkshipPyPI package, typed (PEP 561)
0.1.0pre-1.0 release
3.10+CPython, pure Python (httpx)
serverlocal evaluation, ds_srv_ keys only
Server mode only

The 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#

Shellterminal
$ pip install darkship
$ poetry add darkship
$ uv add darkship
$ pip install "darkship[openfeature]"   # adds the OpenFeature provider

Initialise#

Pythonflags.py
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 raises

Create 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#

Init options. Every request carries Darkship-Sdk: python/<version>.
ArgumentTypeDefaultDescription
sdk_keystrrequiredA ds_srv_ key. Other prefixes raise ValueError at init.
base_urlstrDARKSHIP_BASE_URL, else https://sdk.darkship.techEvaluation host. Must be https (plain http only for loopback, unless allow_insecure_http=True).
allow_insecure_httpboolFalseAllow plain http on a non-loopback host (private network, test double). Logs a warning on the darkship logger: the key travels unencrypted.
streamingboolTrueSSE (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_intervalfloat (s)30.0The 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.
timeoutfloat (s)3.0Bound on every network call.
bootstrapdictNoneA snapshot saved earlier (ds.snapshot()), used until the first fetch succeeds.
offlineboolFalseNever open a socket; serve bootstrap only. For tests and air-gapped jobs.
transporthttpx.BaseTransportNoneInject a transport (proxies, mTLS, httpx.MockTransport in tests).
telemetrybool | dictTrue{"flush_interval": 60.0} to tune; False to turn off counters.

Frameworks and process models#

Initialise after you fork

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.

Pythonmain.py
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)

Evaluate flags#

Pythonevaluate.py
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)
Methods mirror the JavaScript SDK in snake_case. The *_variation names are aliases.
MethodAliasReturnsFlag type
get_boolean_value(key, ctx, default)bool_variationboolboolean
get_string_value(...)string_variationstrstring
get_int_value(...)int_variationintint
get_float_value(...)float_variationfloatfloat
get_json_value(...)json_variationAny (dict, list, ...)json
get_date_value(...)date_variationdatetime (UTC)date
get_*_details(...)*_variation_detailEvaluationDetailsame 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.

Pythoncontext.py
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#

Pythonevents.py
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.

Pythonopenfeature_flags.py
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#

Pythonshutdown.py
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:

Pythonconftest.py
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=True

Offline 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.

Was this helpful?