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.3.0.tar.gz (15.9 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.3.0-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file toggleflag-0.3.0.tar.gz.

File metadata

  • Download URL: toggleflag-0.3.0.tar.gz
  • Upload date:
  • Size: 15.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.5

File hashes

Hashes for toggleflag-0.3.0.tar.gz
Algorithm Hash digest
SHA256 3a4c595adbcaa3fbcf9c1755bff83831890b550943711add05db2b1b5c929f70
MD5 220d11963889b712a3e2deed1b01cd7b
BLAKE2b-256 e8ff33c9e1fee9b0bda27399b47a6591e38fb0322b3561d0551d986f635f8d75

See more details on using hashes here.

File details

Details for the file toggleflag-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: toggleflag-0.3.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.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1c8adb03ec3280d2dcf40b1f1ab70b937f9403db674ddad63aa40c0d5ec1ce80
MD5 e8797e91dc35afebd56dcc1647ee65fc
BLAKE2b-256 25d4b89f579e9db41ef47a631beaeb6ea6b4ad68308406e6306b9a442bf20796

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