Feature Toggles for Python

Gate function execution via appsettings.json โ€” one decorator, no if statements, standard-library core.

PyPI version Python versions License
pip install ftrio
Get started View on GitHub
Ported from .NET. This is the Python port of FtrIO for .NET. The two share the same appsettings.json wire format and toggle-value grammar, so they interoperate. Looking for the C# library? See the .NET docs.
๐ŸŽš๏ธ

Zero call-site noise

Decorate a function with @toggle; it runs only when its toggle is on. No if, no wrapper, no injected service.

๐Ÿ“ฆ

Standard-library core

No third-party dependencies. The Azure App Config provider is the only optional extra.

๐Ÿ“„

appsettings.json

Same section names and value grammar as .NET, so config is cross-language and wire-compatible.

๐ŸŽฒ

Strategy pipeline

Percentage rollouts, A/B buckets, blue-green slots, user and attribute targeting, per-user overrides.

๐ŸŒ

Fail-safe providers

HTTP, env vars, and Azure App Config feed a buffer that flushes to disk โ€” reads survive a provider going offline.

๐Ÿ”

ftrio lint

A CI gate that fails when a @toggle function has no matching key in appsettings.json.

๐Ÿš€ Quickstart

1. Decorate a function

The toggle key defaults to the function's own name.

from ftrio import toggle

@toggle
def send_welcome_email(user):
    ...  # runs only when the "send_welcome_email" toggle is on

2. Add a matching key to appsettings.json

{
    "Toggles": {
        "send_welcome_email": true
    }
}

3. Call it normally

When the toggle is off, the call returns None without running the body.

send_welcome_email(user)  # no-op (returns None) when the toggle is off
No config file? If there is no appsettings.json on disk at all, every toggle reads on โ€” a fresh app stays fully functional before any config exists. A present file missing a key raises ToggleDoesNotExistError; a present key with an uninterpretable value raises ToggleParsedOutOfRangeError.

Async

Use @toggle_async for coroutines. When the toggle is off it returns an awaitable that resolves to None, so await is always safe.

from ftrio import toggle_async

@toggle_async
async def sync_orders():
    ...

await sync_orders()  # resolves to None when off โ€” always awaitable

๐Ÿงฑ The builder pipeline

Plain true/false is the baseline. For richer decisions, build a parser fluently and install it as the ambient parser the decorators use:

from ftrio import ToggleParserProvider

ToggleParserProvider.configure_builder(lambda builder: builder
    .with_context_strategies(context_accessor)  # user targeting + attributes + A/B
    .with_percentage_rollout()                   # "20%"
    .with_blue_green()                           # "blue" / "green" from appsettings.json
    .with_overrides())                           # per-user TogglesOverrides, checked first

Strategies are tried in registration order; the first whose can_handle accepts the raw value owns the decision. BooleanStrategy is always appended last, so plain booleans keep working under any chain.

Toggle-value grammar

ValueStrategyMeaning
true / false / 1 / 0Booleanplain on/off
20%PercentageRollouton for ~20% of calls (random per call)
blue / greenBlueGreenon when it names the active deployment slot
users:alice,bobUserTargetingon for the listed user ids
attribute:plan equals premiumAttributeRuleon when the rule matches the user's attribute
ab:50 / ab:50:saltABTestdeterministic per-user 50% bucket

A/B bucketing is stable and identical to the .NET implementation (SHA-256, first four bytes as a little-endian signed int, absolute value modulo 100) โ€” the same user buckets the same way in both languages.

Per-user overrides

TogglesOverrides win unconditionally, before any strategy runs โ€” handy for QA, support escalations, or dogfooding:

{
    "Toggles": { "NewCheckout": "ab:50" },
    "TogglesOverrides": { "NewCheckout": { "alice": true } }
}

๐ŸŒ Providers and the buffer model

External sources feed a ToggleProviderBuffer, which flushes staged values to appsettings.json atomically on an interval. The file stays the on-disk source of truth, so reads survive a provider going offline (fail-safe).

from ftrio import ToggleProviderBuffer
from ftrio.providers import HttpToggleParser

buffer = ToggleProviderBuffer()
HttpToggleParser("https://flags.example.com/toggles", buffer)  # polls, stages, flushes

Available providers: HttpToggleParser (standard library), EnvironmentVariableToggleParser (standalone or buffer mode), and AzureAppConfigToggleParser (needs the ftrio[azure] extra). Each exposes close() and context-manager support.

CompositeToggleParser chains parsers with first-wins fallthrough โ€” e.g. env-var overrides, then a remote provider, then appsettings.json as the durable fallback.

๐Ÿ” The ftrio lint CLI

The .NET library ships a Roslyn analyzer (diagnostic FTRIO001) that fails the build when a [Toggle] method has no matching key in appsettings.json. The Python equivalent is a CLI you run in CI:

$ ftrio lint path/to/project
path/to/project/mod.py:8: FTRIO001: Function 'MissingOne' is decorated with @toggle but has no entry in the Toggles section of appsettings.json

1 toggle(s) missing from appsettings.json.

It walks the tree with ast, resolves each @toggle / @toggle_async key, and exits non-zero on findings so it can gate a pipeline. Non-project directories (.venv, .git, build, dist, __pycache__, and the usual tool caches) are skipped by default.

$ ftrio lint . --exclude tests --exclude "*_generated.py"
$ ftrio lint . --exclude tests,scripts

Use --no-default-excludes to scan everything, and -v/--verbose to see what is being scanned.

๐Ÿ“„ Configuration

appsettings.json keeps the .NET section names (Toggles, TogglesOverrides, FtrIO) for cross-language and wire compatibility โ€” the HTTP provider returns this exact shape.

{
    "FtrIO": {
        "ReloadOnChange": true,            // re-read on each lookup; live edits apply
        "FlushInterval": 5,
        "Environment": "Production",        // overlays appsettings.Production.json
        "BlueGreen": { "CurrentSlot": "blue", "KnownSlots": "blue,green" }
    }
}

The active environment resolves from FtrIO:Environment, then ASPNETCORE_ENVIRONMENT, then DOTNET_ENVIRONMENT (with FTRIO_ENVIRONMENT as an additive Python-native alias).

Supported Python versions

3.9 3.10 3.11 3.12 3.13

See the PyPI page for the authoritative supported range.

๐ŸŽฎ Playground

Run the bundled playground to watch toggles flip live:

$ python -m playground

It cycles four users every two seconds and prints each toggle's ON/OFF state, honouring live edits to playground/appsettings.json.