Lightweight feature flags with zero dependencies — file, env, or code config
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file toggleflag-0.2.0.tar.gz.
File metadata
- Download URL: toggleflag-0.2.0.tar.gz
- Upload date:
- Size: 15.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
231f71fdf7ae0f6be2127cafe2f88816c5be3895b81837e6882e285b46127266
|
|
| MD5 |
4681e3a441fd551205b1dabf46c79ce9
|
|
| BLAKE2b-256 |
7814b93e8aba496928c1265bb085a7a416430365f8035c63288343df43ada090
|
File details
Details for the file toggleflag-0.2.0-py3-none-any.whl.
File metadata
- Download URL: toggleflag-0.2.0-py3-none-any.whl
- Upload date:
- Size: 10.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e65d348d9c30c1d17146254e77c42294f7634e02447c3b43569e73bed71e6fd
|
|
| MD5 |
3f3e8aab13086f102912f245e15411a5
|
|
| BLAKE2b-256 |
a9c49cc7bf5dd704d63e538cc475f931ee9d55c02d14f4bef9c0a85d9d8461f8
|