Skip to content
NanoFlags
Start free

SDKs

Python SDK

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

Install

sh
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) and websockets. The openfeature extra adds openfeature-sdk 0.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.

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

python
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 key for 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.

python
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, ...}
MethodReturns
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

python
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)
FieldMeaning
valueThe value served (your default in the fallback cases).
reasonrule_match, default (no rule matched), off (flag disabled), flag_not_found, error (wrong type or malformed configuration)
rule_index · rule_idThe matching rule (zero-based index), or None.
in_rollout · bucketFor percentage rollouts: whether the key is inside the allocation, and its bucket (0 to 99.99).
revisionThe configuration revision the result came from.
traceA list of TraceStep explaining the decision step by step.
errorWhy 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.

python
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))
FieldMeaning
revision · previous_revisionThe new revision and the one it replaced (None for the first).
checksumChecksum of the new configuration.
sourcenetwork, store (file cache) or bootstrap
transportlive (pushed), stream (fetched after a server-sent-events notice) or poll
latency_msDatabase 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_error and on("update" | "error" | "status", cb) return an unsubscribe function. You can also pass on_change= and on_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

python
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) (alias ready) blocks until a configuration is in use, or until the first load attempt finished without one. It never raises; it returns True when 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.

python
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_path writes the bundle atomically with mode 0600. On start, it is restored as stale until 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 with get() and set(text) (for example nanoflags.FileStore(path)) and overrides cache_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.

python
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_error receives code signature.
  • A bundle naming an unknown key id (a key rotation) makes the client load GET /v1/keys once, 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=False turns 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

sh
pip install "nanoflags[openfeature]"
python
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_key becomes key. 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 emit PROVIDER_READY and PROVIDER_STALE.
  • close_client=True closes the NanoFlags client when OpenFeature shuts the provider down. Options: ready_timeout (default 10 s).

asyncio

python
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 needed

AsyncNanoFlags 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

OptionDefaultDescription
base_urlhttps://edge.nanoflags.ioEdge URL.
service"unknown"The name this process reports under.
poll_interval30.0Seconds between polls while no socket is open. 0 disables polling.
live_poll_interval300.0Safety poll while the live socket is healthy.
liveTrueFalse skips the WebSocket and uses server-sent events and polling.
streamingTrueFalse leaves polling only.
cache_path · storenoneSee Last good config.
bootstrapnoneA bundle (dict or JSON text) to serve until the edge answers.
verifyon for the hosted edgeVerifyOptions(public_keys, require) or False.
events10 % sampledEventsOptions(enabled, sample_rate, flush_interval, report_interval, redact) or False.
on_change · on_errornoneListeners registered before the first load.
startTrueFalse 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

python
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 use with) so telemetry is flushed.

Troubleshooting

  1. Every variation returns the default. Check flags.status and register on_error before the first load (constructor argument). unauthorized means the key was rejected; with a browser key (nf_pub_…) only client-exposed flags are delivered.
  2. Pre-fork servers (gunicorn, uWSGI, Celery prefork). Threads don't survive fork(). Create the client in each worker after it starts (for example in a post_fork hook or lazily on first use), not at import time in the parent process.
  3. Behind a proxy or with a custom CA. HTTP and the WebSocket honour HTTPS_PROXY and SSL_CERT_FILE. If upgrades are blocked, the client uses server-sent events and polling, and flags.transport shows stream or poll.
  4. 403 "error code: 1010" from your own HTTP calls. The edge refuses requests that carry the default Python-urllib User-Agent. The SDK sends nanoflags-python/<version>, so this only affects code that calls the edge directly (for example OFREP requests or scripts): set your own User-Agent header.
  5. Errors with code signature. The bundle did not verify and was not used. If you pass a custom base_url, pin that edge's keys with VerifyOptions.
  6. Errors with code engine. The configuration needs a newer evaluation engine: upgrade with pip install -U nanoflags.
  7. Errors with code environment_deleted. The key's environment was deleted. The client stops reconnecting and keeps the last configuration as stale; replace the key.
Other languages
Go has a native SDK too: see Go SDK. Java, .NET, PHP, Ruby, Swift, Kotlin and other languages can read NanoFlags flags through an OpenFeature OFREP provider.