greenflags
Official Python SDK for consuming GreenFlags feature flags from any Python service: Django, FastAPI, Flask, Celery workers, scripts, or anything else that runs Python 3.9+.
Zero dependencies — standard library only. Built to minimize billable requests: one network call fetches the whole environment; every read after that is served from memory. Thread-safe by design for multi-worker servers.
Status:
0.1.0, published on PyPI. Full changelog inCHANGELOG.md.
pip install greenflags
Table of Contents
- Why it exists
- Features
- Requirements
- Quick Start
- Usage Guide
- API Reference
- Geofence
- Types
- Error Handling
- Billing Model
- Handling the
api_token - Framework Recipes
- Development
- Versioning
Why it exists
GreenFlags exposes a read endpoint (GET /v1/flags) where every 2xx response counts as a billable read. A naive integration that fetches a flag on every request handler, or on every conditional check, can generate thousands of unnecessary requests and burn through your quota for no reason.
greenflags solves this with a snapshot + cache model:
- One call (
refresh()) fetches every flag in theproject + environmenttied to your API token. - That response is stored in memory.
- Every read after that (
get_flag,is_enabled,get_all_flags,get_snapshot) is local — zero additional requests.
There is intentionally no method to fetch a single flag over the network — that would break the billing model.
Features
- ✅ Zero dependencies —
urllibfrom the standard library, nothing else. - ✅ Snapshot + in-memory cache — billing-safe by design.
- ✅ Thread-safe — one client instance can be shared across WSGI/ASGI workers and threads.
- ✅ Opt-in polling (
start_polling) on a daemon thread — you decide if and how often it refreshes. - ✅ Fail-open — if the network fails, your app keeps working with the last good snapshot (or the
defaultyou set). - ✅ Client-side geofence evaluation — the end-user's location never leaves your process.
- ✅ Fully typed (
py.typed) — mypy/pyright friendly. - ✅ Injectable HTTP layer — trivial to mock in tests, or to swap for
requests/httpxif you prefer.
Requirements
- Python 3.9+.
- A GreenFlags API token, generated from the dashboard for a specific
project + environment. The token determines which flags the SDK sees — there's no separateproject/environmentto pass. See the API docs for the full contract.
Quick Start
from greenflags import GreenFlagsClient
flags = GreenFlagsClient(
url="https://app.greenflags.dev",
api_token="gf_your_token_here",
)
flags.refresh() # 1 billable request — fetches the whole environment
if flags.is_enabled("new-checkout"):
...
Usage Guide
from greenflags import GreenFlagsClient
flags = GreenFlagsClient(url="https://app.greenflags.dev", api_token="gf_...")
# 1. Fetch the initial snapshot (required before reading real flag values)
flags.refresh()
# 2. Read flags — always from memory, never hits the network
enabled = flags.is_enabled("my-feature") # bool sugar
theme = flags.get_flag("theme", default="light") # str
limit = flags.get_flag("rate-limit", default=100) # number
config = flags.get_flag("config", default={}) # json/dict
# 3. List everything available
all_flags = flags.get_all_flags() # list[Flag]
snapshot = flags.get_snapshot() # dict[str, Flag]
# 4. Subscribe to updates (fires on every successful refresh())
unsubscribe = flags.subscribe(lambda snapshot: print("flags updated"))
# 5. Opt-in polling — without this, the SDK NEVER fetches data on its own
flags.start_polling(60) # seconds; every tick = 1 billable request
# 6. Stop polling (the in-memory snapshot is preserved)
flags.stop_polling()
unsubscribe()
Ground rules
get_flag/is_enablednever raise for missing flags —get_flagreturns yourdefaultandis_enabledreturnsFalseuntil data arrives or when the key doesn't exist.refresh()can raiseGreenFlagsError(network error, invalid token, quota exceeded) — wrap it intry/exceptif you want to log failures. The previous snapshot is kept either way.- Create one client per process and share it — don't build a client (or call
refresh()) per request. Refresh once at startup and usestart_pollingonly if you need near-live data. - Polling runs on a daemon thread: it never blocks interpreter shutdown, and a failed tick doesn't break the next one.
API Reference
GreenFlagsClient(...)
GreenFlagsClient(
url: str, # API base URL, trailing slash optional
api_token: str, # token for the environment you're consuming
coordinates: Coordinates | None = None, # optional — enables geofence evaluation
http_get = ..., # optional — inject the HTTP layer: (url, headers) -> (status, body)
)
Methods
| Method | Signature | Description |
|---|---|---|
refresh |
() -> None |
1 request to GET /v1/flags. Replaces the snapshot and notifies subscribers. Raises GreenFlagsError on network/API error. |
get_snapshot |
() -> dict[str, Flag] |
Copy of the current snapshot, keyed by flag key, geofence-evaluated (see Geofence). |
get_all_flags |
() -> list[Flag] |
Every flag in the current snapshot, geofence-evaluated. |
get_flag |
(key, default=None) -> FlagValue |
Evaluated value of one flag. Fail-open: returns default if missing. |
is_enabled |
(key) -> bool |
True only when the flag exists and currently evaluates to True. |
subscribe |
(listener) -> unsubscribe |
listener(snapshot) runs after every successful refresh(). Returns an unsubscribe callable. |
start_polling |
(interval_seconds) -> None |
Automatic refresh() on a daemon thread. Opt-in — no default. Fail-open. |
stop_polling |
() -> None |
Stops polling. The in-memory snapshot is preserved. |
set_coordinates |
(Coordinates | None) -> None |
Sets or clears the end-user location used for geofence evaluation. No network request. |
Geofence
Some flags can carry an optional geofence — a latitude/longitude/radius target configured in the dashboard. When the SDK has coordinates (constructor or set_coordinates), it evaluates each geofenced flag locally — coordinates are never sent to the server:
- Inside the radius (on-edge counts as inside): the flag's normal value is returned.
- Outside the radius: the flag returns its off value —
Falseforbooleanflags,Noneforstring/number/jsonflags. - No coordinates supplied, or no geofence on the flag: the flag's normal value is returned, unaffected.
Fail-open, by design: a geofence is not a security boundary — any caller that omits coordinates sees the flag's normal value regardless of location.
from greenflags import Coordinates
flags.set_coordinates(Coordinates(latitude=19.4326, longitude=-99.1332))
flags.is_enabled("store-promo") # evaluated against the geofence, if any
flags.set_coordinates(None) # back to "ignore geofence" for every flag
Types
FlagType = Literal["boolean", "string", "number", "json"]
FlagValue = bool | str | int | float | dict | None # None: geofenced-off for non-boolean
@dataclass(frozen=True)
class Coordinates:
latitude: float
longitude: float
@dataclass(frozen=True)
class Flag:
key: str
type: FlagType
value: FlagValue
geofence: Geofence | None
class GreenFlagsError(Exception):
code: str # API error code, or NETWORK_ERROR / PARSE_ERROR
message: str
status: int # HTTP status; 0 for network failures
These types mirror the backend contract (GET /v1/flags) exactly.
Error Handling
from greenflags import GreenFlagsError
try:
flags.refresh()
except GreenFlagsError as err:
print(err.code, err.status, err.message)
Codes that GET /v1/flags can actually return:
err.code |
err.status |
Cause |
|---|---|---|
INVALID_TOKEN |
401 | Token missing, invalid, or revoked |
QUOTA_EXCEEDED |
429 | Monthly read quota exhausted |
BILLING_NO_SUBSCRIPTION |
429 | The workspace has no active subscription |
BILLING_CANCELED |
429 | Subscription canceled |
BILLING_PAST_DUE |
429 | Payment past due |
BILLING_TRIAL_EXPIRED |
429 | Trial expired |
BILLING_LIMIT_REACHED |
429 | Billing limit reached |
NETWORK_ERROR |
0 | The request failed before a response was received |
PARSE_ERROR |
response status | Body wasn't valid JSON, or was missing data.flags |
REQUEST_ERROR |
response status | Non-2xx response with no parseable error code |
get_flag(), is_enabled(), get_all_flags() and get_snapshot() never raise any of these — they're always local reads.
Billing Model
Every call to refresh() (manual or from start_polling) is exactly one HTTP request, and every 2xx response counts as one billable read. All flag reads are 100% in memory — zero requests, no matter how many times you call them.
Recommendation: refresh() once at process startup, then start_polling(60) or more if you need periodic updates. With N workers each running its own poller, remember you pay N reads per tick — prefer fewer, longer-lived clients.
Handling the api_token
The token is a secret — treat it like a database password:
- Never hardcode it. Read it from the environment:
os.environ["GREENFLAGS_API_TOKEN"]. - Keep it out of version control (
.env+.gitignore; ship a.env.examplewithout values). - Python services run server-side, so the mobile-extraction concern doesn't apply — but per-token monthly quotas (dashboard → API Tokens) are still a good blast-radius cap for leaked CI logs or misconfigured staging.
import os
from greenflags import GreenFlagsClient
flags = GreenFlagsClient(
url=os.environ.get("GREENFLAGS_URL", "https://app.greenflags.dev"),
api_token=os.environ["GREENFLAGS_API_TOKEN"],
)
Framework Recipes
FastAPI — refresh at startup, poll in the background, read anywhere:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
flags.refresh()
flags.start_polling(60)
yield
flags.stop_polling()
app = FastAPI(lifespan=lifespan)
@app.get("/checkout")
def checkout():
if flags.is_enabled("new-checkout"):
...
Django — call flags.refresh() in AppConfig.ready() and share the module-level client. Celery — same client per worker process; refresh in worker_process_init.
Development
cd sdks/python
PYTHONPATH=src python3 -m pytest # 12 tests, mocked HTTP — no real network
Tests cover envelope parsing, error mapping, geofence evaluation, fail-open behavior, subscriptions, and polling.
Versioning
Semver, while in 0.x: MINOR can include API changes (no stability guarantee yet), PATCH are fixes. Version-by-version detail in CHANGELOG.md.
Related
- API reference
@greenflags/client— JavaScript/TypeScript SDK@greenflags/react— React hooksgreenflags— Dart/Flutter SDK@greenflags/mcp— MCP server for AI agents
Percentage rollout & variants
Server processes serve many users, so identity is per call — resolve rollout/variants for a specific end user with:
value = client.get_flag_for_user("new-checkout", user_id)
enabled = client.is_enabled_for_user("new-checkout", user_id)
get_flag (no user) keeps returning the raw value (fail-open). Bucketing is deterministic and identical across every GreenFlags SDK and the server (docs/rollout-hash-spec.md).
Metadata
Release files for greenflags 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| greenflags-0.3.0.tar.gz | 14.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| greenflags-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 29.2 kB
Release files / greenflags-0.3.0.tar.gz
| Download URL | greenflags-0.3.0.tar.gz |
|---|---|
| Size | 14.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b8574c24a4a6f18d2a3d902b63c7d3c6942237c254d9644fa007809f54706568
|
|
BLAKE2b-256 checksum How to use checksums |
ba4c0c1e5b7751d26f63ef23fc8889f10578b26cdbbfa8d6784eaaacbb52299a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.1
|
Release files / greenflags-0.3.0-py3-none-any.whl
| Download URL | greenflags-0.3.0-py3-none-any.whl |
|---|---|
| Size | 14.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
209e48d2c617981ab890f7620531b2cac783d7d83b640bc893dde53941b16c5d
|
|
BLAKE2b-256 checksum How to use checksums |
65b42f09e5ddc24c6625979994354ac8b4fb18d895f7372aea6053e96d1d6977
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.1
|