toggleflag
Lightweight feature flags for Python — no server, no dependencies.
import toggle
flags = toggle.Flags()
flags.define("new_checkout", default=True)
flags.define("experiment_v2", variants=["control", "treatment"], rollout_pct=30)
flags.is_on("new_checkout") # True
flags.variant("experiment_v2", user_id="u1") # "treatment" (30% of the time)
with flags.context(user_id="u1", country="US"):
flags.is_on("us_only_feature") # checks per-user + per-country
Why toggleflag?
Feature flags are essential for modern software delivery. They enable teams to ship code behind toggles, decouple deployment from release, run A/B experiments, and instantly disable broken features in production.
Existing solutions have significant trade-offs:
| Concern | toggleflag | LaunchDarkly | Unleash | django-flags |
|---|---|---|---|---|
| Requires a server | ❌ No | ✅ Yes | ✅ Yes | ❌ No |
| External dependencies | Zero | SDK dependency | SDK dependency | Django framework |
| Setup time | < 1 minute |
Account + SDK setup | Self-hosted cluster | Django project |
| Per-user targeting | ✅ | ✅ | ✅ | Limited |
| Percentage rollout | ✅ Deterministic | ✅ | ✅ | ❌ |
| Country targeting | ✅ | ✅ (paid) | ✅ (enterprise) | ❌ |
| A/B testing variants | ✅ | ✅ (paid) | ✅ | ❌ |
| Context propagation | ✅ | ✅ | ✅ | ❌ |
| Live config reload | ✅ File watching | ✅ Streaming | ✅ Polling | ✅ |
| Thread-safe | ✅ | ✅ | ✅ | ✅ |
| Pricing | Free (MIT) | $10–$500+/mo | Free (OSS) / Enterprise | Free |
| Python-only | ✅ | Multi-language | Multi-language | Python (Django) |
toggleflag fills a gap: a zero-dependency, pure-Python feature flag library that supports sophisticated targeting rules without requiring any infrastructure.
Installation
pip install toggleflag
That's it. No servers to configure. No databases to provision. No accounts to create.
Quick Start
Boolean Flags
import toggle
flags = toggle.Flags()
flags.define("dark_mode", default=True)
flags.define("maintenance_mode", default=False)
flags.is_on("dark_mode") # True
flags.is_off("maintenance_mode") # True
Percentage Rollout
flags.define("new_checkout", default=False, rollout_pct=50)
# Deterministic: same user always gets the same result
flags.is_on("new_checkout", user_id="alice") # True (consistent)
flags.is_on("new_checkout", user_id="bob") # False (consistent)
A/B Testing with Variants
flags.define("pricing_page", variants=["control", "variant_a", "variant_b"], rollout_pct=40)
# Returns a variant string, or None if the user is outside the rollout
flags.variant("pricing_page", user_id="u1") # "variant_a"
flags.variant("pricing_page", user_id="u2") # None (outside 40% rollout)
Per-User Targeting
# Beta access for specific users
flags.define("beta_features", default=False, user_ids=["alice", "bob", "charlie"])
flags.is_on("beta_features", user_id="alice") # True
flags.is_on("beta_features", user_id="dave") # False
# Per-user overrides (force on/off regardless of other rules)
flags.define("experiment", default=False, rollout_pct=10,
overrides={"alice": True, "bob": False})
flags.is_on("experiment", user_id="alice") # True (forced on)
flags.is_on("experiment", user_id="bob") # False (forced off)
Country Targeting
flags.define("gdpr_consent_flow", default=True, countries=["DE", "FR", "GB"])
flags.is_on("gdpr_consent_flow", country="US") # False
flags.is_on("gdpr_consent_flow", country="DE") # True
Context Propagation
with flags.context(user_id="alice", country="US"):
# All flag checks within this block use alice/US context
flags.is_on("beta_features") # True (alice is in user_ids)
flags.is_on("us_only_promo") # True (US country match)
flags.variant("experiment_v2") # Deterministic variant for alice
# Nested contexts merge with parent
with flags.context(country="DE"):
flags.is_on("gdpr_consent_flow") # True (DE, alice's user_id preserved)
# Explicit arguments override context
with flags.context(user_id="alice"):
flags.is_on("beta_features", user_id="bob") # False (explicit arg wins)
File-Based Configuration
{
"new_checkout": {"default": true, "rollout_pct": 50},
"dark_mode": {"default": true},
"experiment_v2": {
"variants": ["control", "treatment"],
"rollout_pct": 30,
"countries": ["US", "GB"]
},
"beta_features": {
"default": false,
"user_ids": ["alice", "bob"]
}
}
flags = toggle.Flags()
flags.load_file("flags.json")
Environment Variables
export TOGGLE_DARK_MODE=true
export TOGGLE_NEW_CHECKOUT=50
export TOGGLE_EXPERIMENT=50:treatment,control
flags = toggle.Flags()
flags.load_env()
# Custom prefix
flags.load_env(prefix="APP_") # reads APP_DARK_MODE=true, etc.
Auto-Reload File Changes
watcher = flags.watch("flags.json", poll_interval=2.0)
# In your request handler or background loop:
if watcher.changed():
flags.load(watcher.read())
print("Flags reloaded!")
Deterministic Rollout
Percentage-based targeting in toggleflag is deterministic — the same user always gets the same result, across requests, servers, and restarts.
This works by computing MD5(flag_name + ":" + user_id) % 100 and comparing against rollout_pct. The result depends only on the flag name and user identifier, not on random state.
flags.define("experiment", variants=["control", "treatment"], rollout_pct=30)
# These calls always return the same value for the same user
flags.variant("experiment", user_id="alice") # Always "treatment"
flags.variant("experiment", user_id="bob") # Always None
Why MD5? MD5 is fast (important for request-time evaluation), uniformly distributed, and deterministic. While not cryptographically secure, that's irrelevant here — we're bucketing users, not protecting secrets.
No user ID? When no user identifier is provided, the system uses a default sentinel value, meaning all anonymous users get the same result. For meaningful rollouts, always provide a user_id.
API Reference
Flags()
Create a new flag engine instance. Thread-safe.
flags.define(name, *, default=False, rollout_pct=?, variants=None, user_ids=None, countries=None, overrides=None)
Define a feature flag.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
— | Flag identifier |
default |
bool |
False |
Default on/off state (used when no rollout_pct is set) |
rollout_pct |
int |
auto | 0–100+ percentage of users who see the flag |
variants |
list[str] |
[] |
Variant names for A/B testing |
user_ids |
list[str] |
None |
Allow-list of user IDs |
countries |
list[str] |
None |
Allow-list of country codes |
overrides |
dict |
{} |
Per-user forced values {"user_id": True/False/"variant"} |
flags.is_on(name, *, user_id=None, country=None) → bool
Check if a flag is on for the given context.
flags.is_off(name, **kwargs) → bool
Inverse of is_on.
flags.variant(name, *, user_id=None, country=None) → str | None
Return the A/B variant for the user, or None if outside rollout.
flags.context(*, user_id=None, country=None, **extra) → ContextManager
Push a temporary evaluation context. Nestable — child contexts merge with parents.
flags.load(data: dict) / flags.load_file(path) / flags.load_env(prefix="TOGGLE_")
Bulk-load flag definitions from a dict, JSON/YAML file, or environment variables.
flags.watch(path, poll_interval=2.0) → FileWatcher
Create a file watcher for live config reloading.
flags.list_flags() → list[str] / flags.get(name) → FlagDef | None / flags.undefine(name) / flags.clear()
Introspection and management methods.
Design Philosophy
-
Zero dependencies. Pure Python 3.10+. No external packages. No database. No network calls.
pip installand go. -
Deterministic by default. Same user, same flag, same result — always. No random state to corrupt or drift.
-
Configuration flexibility. Programmatic API, JSON/YAML files, environment variables, or any combination. Use what fits your deployment.
-
Targeting composure. User IDs, countries, percentages, and overrides compose naturally. Each rule is a filter; they stack.
-
No infrastructure required. Unlike LaunchDarkly or Unleash, toggleflag needs no server, database, or cluster. A JSON file is sufficient for production use.
-
Thread-safe. All flag reads and writes are protected by locks. Safe for multi-threaded web servers (Django, Flask, FastAPI).
-
Explicit is better than implicit. Flags must be defined before use. Undefined flags return
Falserather than raising errors — fail soft in production.
Use Cases
A/B Testing
flags.define("pricing_page", variants=["control", "variant_a", "variant_b"], rollout_pct=50)
variant = flags.variant("pricing_page", user_id=current_user.id)
if variant == "variant_a":
return render_pricing_v2(request)
return render_pricing(request)
Phased Rollout
# Week 1: 10% of users
flags.define("new_api", default=False, rollout_pct=10)
# Week 2: Increase to 50%
flags.define("new_api", default=False, rollout_pct=50)
# Week 3: Full launch
flags.define("new_api", default=True)
Canary Releases
flags.define("v2_engine", default=False, user_ids=["alice", "bob", "qa_team"])
if flags.is_on("v2_engine", user_id=request.user.id):
return v2_handler(request)
return v1_handler(request)
Kill Switches
# Disable a feature instantly without redeploying
flags.define("external_payments", default=True)
# In a config file — flip to false to disable
# {"external_payments": {"default": false}}
Country-Specific Features
flags.define("eu_compliance", default=True, countries=["DE", "FR", "IT", "ES", "GB"])
flags.define("us_tax_form", default=True, countries=["US"])
Beta Programs
flags.define("beta_ai_features", default=False, user_ids=beta_users)
Testing
pip install -e ".[dev]"
pytest tests/ -v
87 tests covering all targeting rules, context propagation, file/env loading, thread safety, and edge cases.
License
MIT © Ravi Teja Prabhala Venkata
Metadata
Release files for toggleflag 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 | Provenance | Actions |
|---|---|---|---|---|
| toggleflag-0.3.0.tar.gz | 15.9 kB | Without provenance | Details for toggleflag-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toggleflag-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for toggleflag-0.3.0-py3-none-any.whl |
Total release size: 25.9 kB
Release files / toggleflag-0.3.0.tar.gz
| Download URL | toggleflag-0.3.0.tar.gz |
|---|---|
| Size | 15.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3a4c595adbcaa3fbcf9c1755bff83831890b550943711add05db2b1b5c929f70
|
|
BLAKE2b-256 checksum How to use checksums |
e8ff33c9e1fee9b0bda27399b47a6591e38fb0322b3561d0551d986f635f8d75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.5
|
Release files / toggleflag-0.3.0-py3-none-any.whl
| Download URL | toggleflag-0.3.0-py3-none-any.whl |
|---|---|
| Size | 10.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1c8adb03ec3280d2dcf40b1f1ab70b937f9403db674ddad63aa40c0d5ec1ce80
|
|
BLAKE2b-256 checksum How to use checksums |
25d4b89f579e9db41ef47a631beaeb6ea6b4ad68308406e6306b9a442bf20796
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.5
|
Security for toggleflag 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.