SDKs
Python SDK
nanoflags package evaluates flags locally and synchronously against a signed, in-memory configuration. Background threads keep it current over the live connection. A variation call never performs I/O and never raises.On this page
Install
pip install nanoflags
pip install "nanoflags[openfeature]" # with the OpenFeature provider- Published on PyPI as nanoflags 0.1.0. Python 3.9 or newer, fully typed (
py.typed). - Dependencies:
cryptography(Ed25519) andwebsockets. Theopenfeatureextra addsopenfeature-sdk0.8 or newer. - The engine is a pure-Python port of the NanoFlags engine. It is tested against the shared fixtures generated from the reference engine (21,998 evaluation cases), and gives the same results as the JavaScript and Go SDKs.
- Apache-2.0. Source in the nanoflags-sdk repository.
Quickstart
Copy a server key (nf_srv_…) from Settings → Keys in the dashboard and keep it in an environment variable.
import os
from nanoflags import NanoFlags
flags = NanoFlags(
os.environ["NANOFLAGS_SDK_KEY"], # nf_srv_… (server view) or nf_pub_… (client-exposed flags only)
service="checkout-api", # shown in "which revision is each service running"
)
flags.wait_until_ready(timeout=5) # never raises; True once a configuration is in use
user = {"key": "user-42", "plan": "beta", "region": "DE"}
if flags.bool_variation("new-checkout", False, user):
...
flags.close() # on shutdown (or use `with NanoFlags(...) as flags:`)Create one client per process and share it between threads. The base URL defaults to the hosted edge, https://edge.nanoflags.io.
Contexts
The context is a dict of JSON-like values (strings, numbers, booleans, None, lists and dicts). key is the stable identity used for percentage rollouts, usually a user or account id. Every other attribute is matched by your targeting rules.
ctx = {"key": account.id, "plan": account.plan, "country": "SA", "beta": True, "seats": 12}
flags.string_variation("search-ranking", "relevance-v2", ctx)- The context never leaves the process to make a decision. See telemetry for what diagnostics send.
- Pass the same
keyfor the same user everywhere: the bucket is computed from it, so the same key lands in the same rollout bucket in Python, Go, JavaScript and the dashboard.
Typed variations
Each variation takes the flag key, the default, and an optional context. The default is served when the flag cannot be evaluated.
enabled = flags.bool_variation("new-checkout", False, user)
accent = flags.string_variation("accent", "blue", user)
limit = flags.number_variation("rate-limit", 100, user) # int or float
layout = flags.json_variation("layout", {"cols": 3}, user) # any JSON value
every = flags.all_flags(user) # {"new-checkout": True, ...}| Method | Returns |
|---|---|
bool_variation(key, default, context=None) | a bool |
string_variation(key, default, context=None) | a str |
number_variation(key, default, context=None) | an int or float (booleans and non-finite numbers are not accepted) |
json_variation(key, default, context=None) | any JSON value: dict, list, str, number, bool or None |
*_variation_detail(key, default, context=None) | an Evaluation, see Details and reasons |
evaluate(key, default, context=None) | the engine result without a type check |
all_flags(context=None) | every flag of the loaded configuration as a dict ({} without one; not sampled into telemetry) |
You get your default when no configuration has loaded yet, when the flag is unknown (or, with a browser key, not client-exposed), or when the flag serves a value of another type. A disabled flag is not a fallback case: it returns the flag's configured off value.
Details and reasons
detail = flags.bool_variation_detail("new-checkout", False, user)
print(detail.value, detail.reason, detail.rule_id, detail.revision) # True rule_match beta 57
for step in detail.trace: # the same steps the dashboard shows
print(step.kind, step.title, step.detail)| Field | Meaning |
|---|---|
value | The value served (your default in the fallback cases). |
reason | rule_match, default (no rule matched), off (flag disabled), flag_not_found, error (wrong type or malformed configuration) |
rule_index · rule_id | The matching rule (zero-based index), or None. |
in_rollout · bucket | For percentage rollouts: whether the key is inside the allocation, and its bucket (0 to 99.99). |
revision | The configuration revision the result came from. |
trace | A list of TraceStep explaining the decision step by step. |
error | Why reason is error. |
Live updates and on_change
The client opens one WebSocket to wss://edge.nanoflags.io/v1/live (the key travels in the Authorization header, never in the URL). The edge pushes each published revision, already signed; the client verifies it and swaps it in atomically, so an evaluation always sees one revision in full.
def changed(e):
print(f"now on r{e.revision} (was r{e.previous_revision}) via {e.transport}, {e.latency_ms} ms after commit")
unsubscribe = flags.on_change(changed)
flags.on_error(lambda err: log.warning("nanoflags %s: %s", err.code, err))| Field | Meaning |
|---|---|
revision · previous_revision | The new revision and the one it replaced (None for the first). |
checksum | Checksum of the new configuration. |
source | network, store (file cache) or bootstrap |
transport | live (pushed), stream (fetched after a server-sent-events notice) or poll |
latency_ms | Database commit to this event, for pushed revisions that carry their commit time; otherwise None. It compares two clocks, so it is only as accurate as they agree. |
- Listeners run on the client's background thread: keep them short, and hand longer work to your own queue. An exception in a listener is logged and never breaks the client.
on_change,on_errorandon("update" | "error" | "status", cb)return an unsubscribe function. You can also passon_change=andon_error=to the constructor so the first load is not missed.- Fallbacks: if the socket cannot be opened, the client uses server-sent events (
/v1/stream) and polls every 30 s. While the socket is healthy, a safety poll runs every 5 minutes. Reconnects back off from 1 s up to 5 minutes, with jitter. See Transports for the protocol.
Readiness and status
if not flags.wait_until_ready(timeout=5):
log.warning("flags not loaded yet; variations return their defaults")
flags.status # "not-ready" | "ready" | "stale"
flags.revision # revision in use, or None
flags.transport # "live" | "stream" | "poll"wait_until_ready(timeout)(aliasready) blocks until a configuration is in use, or until the first load attempt finished without one. It never raises; it returnsTruewhen a configuration is in use.ready: the configuration was confirmed by the edge on the last attempt.stale: a configuration is in use but is not known to be current (restored from the cache and not yet confirmed, or the edge could not be reached).not-ready: nothing is loaded and every variation returns its default.- Don't block a request on readiness. Wait once at start-up, with a short timeout, and let your defaults cover the rest.
Last good config and bootstrap
Once loaded, the last good configuration stays in use through network failures, rejected bundles and edge errors (status becomes stale). The client never replaces a configuration with an older revision.
flags = NanoFlags(
sdk_key,
cache_path="/var/cache/app/flags.json", # keep the last good bundle across restarts
)
# Or start from a bundle you ship (dict or JSON text), served until the edge answers:
flags = NanoFlags(sdk_key, bootstrap=open("flags-bootstrap.json").read())cache_pathwrites the bundle atomically with mode0600. On start, it is restored asstaleuntil the edge confirms it. A restored copy is verified again and ignored if it does not verify, or if it was written for another key or edge.store=takes any object withget()andset(text)(for examplenanoflags.FileStore(path)) and overridescache_path.- The cache holds flag rules, never the SDK key. Bootstrap is useful for read-only file systems and for tests.
Signature verification
Every bundle is checked (Ed25519, by key id) before it can replace the one in use: over the live socket, over HTTP and from the cache. With the hosted edge this is on by default with the production keys embedded in the package (OFFICIAL_SIGNING_KEYS), and unsigned bundles are refused. The signed bytes are hashed exactly as served.
from nanoflags import NanoFlags, VerifyOptions
# Self-hosted or proxied edge: pin your keys and require signatures
flags = NanoFlags(
sdk_key,
base_url="https://flags.internal.example",
verify=VerifyOptions(public_keys=[{"kty": "OKP", "crv": "Ed25519", "x": "…", "kid": "k1"}], require=True),
)- A bundle that fails verification is not used: the previous one stays, and
on_errorreceives codesignature. - A bundle naming an unknown key id (a key rotation) makes the client load
GET /v1/keysonce, at most once a minute. public_keys="fetch"loads keys from the edge instead of pinning them. Pinned keys are stronger: fetched keys are only as trustworthy as the edge that served them.verify=Falseturns verification off. The canonical string and headers are described in Signatures.- A bundle written for a newer evaluation engine than the package supports is refused (code
engine) and the current configuration stays: upgrade the package.
OpenFeature provider
pip install "nanoflags[openfeature]"from openfeature import api
from openfeature.evaluation_context import EvaluationContext
from nanoflags import NanoFlags
from nanoflags.openfeature import NanoFlagsProvider
provider = NanoFlagsProvider(NanoFlags(sdk_key, service="checkout-api"), close_client=True)
api.set_provider_and_wait(provider) # openfeature-sdk >= 0.9; on 0.8 use api.set_provider(provider)
client = api.get_client()
client.get_boolean_value("new-checkout", False, EvaluationContext(targeting_key="user-42", attributes={"plan": "beta"}))- Evaluation stays local: the provider wraps the client above, so live updates, signatures and the last good config work the same.
targeting_keybecomeskey. Reasons: disabled flag →DISABLED, no rule matched →DEFAULT, fixed rule →TARGETING_MATCH, rollout →SPLIT(the variant is the rule id). Errors:FLAG_NOT_FOUND,TYPE_MISMATCH.- The float accessor accepts whole numbers. New revisions emit
PROVIDER_CONFIGURATION_CHANGED; status changes emitPROVIDER_READYandPROVIDER_STALE. close_client=Truecloses the NanoFlags client when OpenFeature shuts the provider down. Options:ready_timeout(default 10 s).
asyncio
from nanoflags.aio import AsyncNanoFlags
async with AsyncNanoFlags(sdk_key, service="api") as flags:
await flags.wait_until_ready(5)
enabled = flags.bool_variation("new-checkout", False, {"key": user_id}) # local, no await neededAsyncNanoFlags takes the same keyword arguments. Variations stay synchronous; wait_until_ready, refresh, flush and close are awaitable. on_change accepts coroutine functions, which run on the event loop that created the client.
Options
| Option | Default | Description |
|---|---|---|
base_url | https://edge.nanoflags.io | Edge URL. |
service | "unknown" | The name this process reports under. |
poll_interval | 30.0 | Seconds between polls while no socket is open. 0 disables polling. |
live_poll_interval | 300.0 | Safety poll while the live socket is healthy. |
live | True | False skips the WebSocket and uses server-sent events and polling. |
streaming | True | False leaves polling only. |
cache_path · store | none | See Last good config. |
bootstrap | none | A bundle (dict or JSON text) to serve until the edge answers. |
verify | on for the hosted edge | VerifyOptions(public_keys, require) or False. |
events | 10 % sampled | EventsOptions(enabled, sample_rate, flush_interval, report_interval, redact) or False. |
on_change · on_error | none | Listeners registered before the first load. |
start | True | False defers background work until start(). |
Error codes
Background failures reach on_error as NanoFlagsError with a code (and http_status where relevant): network, unauthorized, http, invalid_bundle, store, config, signature, session, engine, environment_deleted. Nothing is ever raised from a variation call. The meanings match the JavaScript SDK.
Telemetry
A sampled 10 % of evaluations are sent every 10 s, plus a service report every 60 s. By default, context values never leave the process: only the names (and value types) of the attributes the flag's rules use. events=EventsOptions(redact=...) replaces this; events=False turns telemetry off.
Closing and shutdown
import atexit
atexit.register(flags.close)
# or scope it
with NanoFlags(sdk_key, service="nightly-job") as flags:
run(flags)close()stops the background threads and sends queued telemetry (best effort, with a short timeout). It is safe to call more than once.- After
close(), variations keep working on the last configuration; it just stops updating. - Background threads are daemon threads, so they don't keep a script alive. Short scripts should still call
close()(or usewith) so telemetry is flushed.
Troubleshooting
- Every variation returns the default. Check
flags.statusand registeron_errorbefore the first load (constructor argument).unauthorizedmeans the key was rejected; with a browser key (nf_pub_…) only client-exposed flags are delivered. - Pre-fork servers (gunicorn, uWSGI, Celery prefork). Threads don't survive
fork(). Create the client in each worker after it starts (for example in apost_forkhook or lazily on first use), not at import time in the parent process. - Behind a proxy or with a custom CA. HTTP and the WebSocket honour
HTTPS_PROXYandSSL_CERT_FILE. If upgrades are blocked, the client uses server-sent events and polling, andflags.transportshowsstreamorpoll. 403"error code: 1010" from your own HTTP calls. The edge refuses requests that carry the defaultPython-urllibUser-Agent. The SDK sendsnanoflags-python/<version>, so this only affects code that calls the edge directly (for example OFREP requests or scripts): set your ownUser-Agentheader.- Errors with code
signature. The bundle did not verify and was not used. If you pass a custombase_url, pin that edge's keys withVerifyOptions. - Errors with code
engine. The configuration needs a newer evaluation engine: upgrade withpip install -U nanoflags. - Errors with code
environment_deleted. The key's environment was deleted. The client stops reconnecting and keeps the last configuration asstale; replace the key.