Skip to main content

Lightweight feature flags with zero dependencies — file, env, or code config

Project description

toggleflag

Python 3.10+ MIT License Tests

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

  1. Zero dependencies. Pure Python 3.10+. No external packages. No database. No network calls. pip install and go.

  2. Deterministic by default. Same user, same flag, same result — always. No random state to corrupt or drift.

  3. Configuration flexibility. Programmatic API, JSON/YAML files, environment variables, or any combination. Use what fits your deployment.

  4. Targeting composure. User IDs, countries, percentages, and overrides compose naturally. Each rule is a filter; they stack.

  5. No infrastructure required. Unlike LaunchDarkly or Unleash, toggleflag needs no server, database, or cluster. A JSON file is sufficient for production use.

  6. Thread-safe. All flag reads and writes are protected by locks. Safe for multi-threaded web servers (Django, Flask, FastAPI).

  7. Explicit is better than implicit. Flags must be defined before use. Undefined flags return False rather 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toggleflag-0.2.0.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toggleflag-0.2.0-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

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

Hashes for toggleflag-0.2.0.tar.gz
Algorithm Hash digest
SHA256 231f71fdf7ae0f6be2127cafe2f88816c5be3895b81837e6882e285b46127266
MD5 4681e3a441fd551205b1dabf46c79ce9
BLAKE2b-256 7814b93e8aba496928c1265bb085a7a416430365f8035c63288343df43ada090

See more details on using hashes here.

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

Hashes for toggleflag-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7e65d348d9c30c1d17146254e77c42294f7634e02447c3b43569e73bed71e6fd
MD5 3f3e8aab13086f102912f245e15411a5
BLAKE2b-256 a9c49cc7bf5dd704d63e538cc475f931ee9d55c02d14f4bef9c0a85d9d8461f8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page