Feature flags
Project description
fflgs
Feature flag evaluation with hierarchical rule-based architecture. Sync/async, type-safe, extensible.
Motivation
Existing Python feature flag libraries fall into two categories: they're either SDKs for paid third-party services, or they lack proper rule-based evaluation capabilities. fflgs was created as a simple, easy to use, rule-based feature flag library suitable for hobby projects.
Quick Start
from fflgs.core import Condition, Rule, RuleGroup, Flag, FeatureFlags
from fflgs.providers.memory import InMemoryProvider
# Build your flag
flag = Flag(
name="webhooks",
description="Allow webhooks for enterprise users",
rules_strategy="ALL",
rule_groups=[
RuleGroup(
operator="AND",
rules=[
Rule(
operator="AND",
conditions=[
Condition("user.plan", "IN", ["pro", "enterprise"], active=True)
],
active=True
)
],
active=True
)
],
enabled=True,
version=1
)
# Evaluate
provider = InMemoryProvider()
provider.add_flag(flag)
ff = FeatureFlags(provider)
context = {"user": {"plan": "pro"}}
ff.is_enabled("webhooks", ctx=context) # True
Architecture
Evaluation hierarchy
graph TD
A["Condition<br/>(single attribute test)"]
B["Rule<br/>(AND/OR conditions)"]
C["RuleGroup<br/>(AND/OR rules)"]
D["Flag<br/>(ALL/ANY/NONE strategy)"]
A -->|multiple| B
B -->|multiple| C
C -->|multiple| D
fflgs architecture
graph TB
subgraph Providers["Providers"]
direction LR
P1["InMemoryProvider"]
P2["JSONProvider"]
P3["YAMLProvider"]
P4["Custom Provider"]
end
subgraph Core["Core Evaluation"]
FF["FeatureFlags"]
FFA["FeatureFlagsAsync"]
end
subgraph Caching["Caching Layer"]
CFF["CachedFeatureFlags"]
CFFA["CachedFeatureFlagsAsync"]
subgraph Storage["Storage"]
S1["InMemoryStorage"]
S2["Custom Storage"]
end
end
Providers -->|get_flag| FF
Providers -->|get_flag| FFA
FF -->|evaluate| Flag["Flag<br/>(evaluation)"]
FFA -->|evaluate| Flag
FF -->|wrap| CFF
FFA -->|wrap| CFFA
CFF -->|cache| S1
CFFA -->|cache| S1
Operators
| Operator | Logic |
|---|---|
EQUALS / NOT_EQUALS |
Equality |
GREATER_THAN / LESS_THAN / GREATER_THAN_OR_EQUALS / LESS_THAN_OR_EQUALS |
Comparison |
IN / NOT_IN |
Containment |
CONTAINS / NOT_CONTAINS |
Container containment |
REGEX |
Pattern matching |
Strategies
- ALL: Every rule group must pass (AND)
- ANY: At least one rule group passes (OR)
- NONE: All rule groups fail (NOR)
Context Access
Dot notation supports nested dicts and object attributes:
context = {
"user": {"profile": {"role": "admin"}},
"request": {"ip": "192.168.1.1"}
}
Condition("user.profile.role", "EQUALS", "admin", active=True)
Condition("request.ip", "REGEX", r"192\.168\..*", active=True)
Providers
Built-in Providers
JSON Provider
Load feature flags from a JSON file:
from fflgs.providers.json import JSONProvider
provider = JSONProvider(
"flags.json",
cache_enabled=True, # Cache file contents (default)
cache_ttl_seconds=None, # Cache indefinitely
)
ff = FeatureFlags(provider)
Disable file caching or set TTL:
# Reload from file every call
provider = JSONProvider("flags.json", cache_enabled=False)
# Reload after 60 seconds
provider = JSONProvider("flags.json", cache_ttl_seconds=60)
YAML Provider
Load feature flags from a YAML file:
from fflgs.providers.yaml import YAMLProvider
provider = YAMLProvider(
"flags.yaml",
cache_enabled=True,
cache_ttl_seconds=None,
)
ff = FeatureFlags(provider)
Configuration options match JSON provider.
Async Providers
All built-in providers have async versions:
from fflgs.providers.json import JSONProviderAsync
from fflgs.providers.yaml import YAMLProviderAsync
async def check():
provider = JSONProviderAsync("flags.json")
ff = FeatureFlagsAsync(provider)
return await ff.is_enabled("flag_name", ctx=context)
Caching
Optional in-memory caching layer to improve performance. Two caching levels are available:
- Provider-level caching (built-in to JSON/YAML providers) - Caches file contents
- Result caching (CachedFeatureFlags wrapper) - Caches evaluation results per flag and context
Result Caching
Wrap your FeatureFlags instance to cache evaluation results:
from fflgs.cache.memory import InMemoryStorage
from fflgs.cache.wrapper import CachedFeatureFlags
provider = InMemoryProvider()
ff = FeatureFlags(provider)
storage = InMemoryStorage()
cached_ff = CachedFeatureFlags(
ff,
storage=storage,
default_ttl=300, # 5 minutes (required)
ttl_per_flag={"critical_flag": 60} # Override default TTL per flag
)
result = cached_ff.is_enabled("webhooks", ctx=context) # Cached result
Clear the cache:
# Clear entire cache
cached_ff.clear_cache()
# Clear specific cache entry
from fflgs.cache._utils import generate_cache_key
key = generate_cache_key("my_flag", version=1, ctx={"user_id": 123})
cached_ff.clear_cache(cache_key=key)
Async version:
from fflgs.cache.wrapper import CachedFeatureFlagsAsync
cached_ff = CachedFeatureFlagsAsync(ff_async, storage=storage, default_ttl=300)
result = await cached_ff.is_enabled("webhooks", ctx=context)
# Clearing works the same way
await cached_ff.clear_cache()
Async
from fflgs.core import FeatureFlagsAsync
from fflgs.providers.memory import InMemoryProviderAsync
async def check():
provider = InMemoryProviderAsync()
provider.add_flag(flag)
ff = FeatureFlagsAsync(provider)
return await ff.is_enabled("webhooks", ctx=context)
Error Handling
ff = FeatureFlags(
provider,
on_flag_not_found="raise", # or "return_false"
on_evaluation_error="return_false",
on_provider_error="return_false" # Handle provider exceptions
)
# Override per-call
ff.is_enabled("flag", ctx=context, on_flag_not_found="raise")
Error handling options:
on_flag_not_found: Missing flag in provideron_evaluation_error: Malformed rules or context issueson_provider_error: Provider exceptions (DB down, network error, etc.)
Each option accepts:
"raise": Raise the corresponding exception"return_false": ReturnFalseand log warning (default, safe for production)
Custom Provider
Implement the FeatureFlagsProvider protocol:
from fflgs.core import Flag, FeatureFlagsProviderError
class MyProvider:
def get_flag(self, flag_name: str) -> Flag | None:
"""Fetch flag from database, API, or custom source.
Returns:
Flag object if found, None if not found
Raises:
FeatureFlagsProviderError: If an error occurs while fetching
"""
try:
# Fetch from DB, API, etc.
flag = fetch_flag_from_database(flag_name)
return flag
except Exception as exc:
raise FeatureFlagsProviderError(f"Failed to fetch flag: {exc}") from exc
ff = FeatureFlags(MyProvider())
For async, implement FeatureFlagsProviderAsync:
class MyAsyncProvider:
async def get_flag(self, flag_name: str) -> Flag | None:
"""Async version of get_flag."""
try:
return await db.query_flag(flag_name)
except Exception as exc:
raise FeatureFlagsProviderError(f"Failed to fetch flag: {exc}") from exc
ff = FeatureFlagsAsync(MyAsyncProvider())
Important: Always raise FeatureFlagsProviderError for any errors encountered while fetching flags. This allows proper error handling through the on_provider_error configuration.
Development
For detailed development setup, running tests, code quality checks, and contribution guidelines, see CONTRIBUTING.md.
Examples
Rule(
operator="AND",
conditions=[
Condition("user.age", "GREATER_THAN", 21, True),
Condition("user.region", "IN", ["US", "CA"], True),
],
active=True
)
Condition("user.id", "REGEX", r"^[0-4]", True)
RuleGroup(
operator="OR",
rules=[
Rule(operator="AND", conditions=[Condition("user.beta", "EQUALS", True, True)], active=True),
Rule(operator="AND", conditions=[Condition("user.role", "EQUALS", "admin", True)], active=True),
],
active=True
)
License
MIT - See LICENSE.txt
Credits
This package was created with The Hatchlor project template.
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 fflgs-0.0.0a1.tar.gz.
File metadata
- Download URL: fflgs-0.0.0a1.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8c9fa8f4951c383127dfb5158cd1ce1c45b98f12f2c7dbe6a6ec3ab19531835
|
|
| MD5 |
e8d94d809e83f60d927f160554bc6b45
|
|
| BLAKE2b-256 |
cb9d30cf0e43ef254fb695236aef6e4f51d74b80d6dfb45ed622afc4b5f1dee6
|
Provenance
The following attestation bundles were made for fflgs-0.0.0a1.tar.gz:
Publisher:
build.yml on bartosz121/fflgs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fflgs-0.0.0a1.tar.gz -
Subject digest:
d8c9fa8f4951c383127dfb5158cd1ce1c45b98f12f2c7dbe6a6ec3ab19531835 - Sigstore transparency entry: 760405834
- Sigstore integration time:
-
Permalink:
bartosz121/fflgs@d4f7c5b68914e130e33bbe80fadbff8261490c80 -
Branch / Tag:
refs/tags/v0.0.0a1 - Owner: https://github.com/bartosz121
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@d4f7c5b68914e130e33bbe80fadbff8261490c80 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fflgs-0.0.0a1-py3-none-any.whl.
File metadata
- Download URL: fflgs-0.0.0a1-py3-none-any.whl
- Upload date:
- Size: 21.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eab62f622b5988033a70ec0834b83db8def78280dcf57be59e5297b3e6975ca8
|
|
| MD5 |
9f9a32f0bc70b968b14dc6c3d5cad672
|
|
| BLAKE2b-256 |
3944dfbe408f7c568f10e61ca121e523130360468e8ceccbc4f8da7f50a320d9
|
Provenance
The following attestation bundles were made for fflgs-0.0.0a1-py3-none-any.whl:
Publisher:
build.yml on bartosz121/fflgs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fflgs-0.0.0a1-py3-none-any.whl -
Subject digest:
eab62f622b5988033a70ec0834b83db8def78280dcf57be59e5297b3e6975ca8 - Sigstore transparency entry: 760405838
- Sigstore integration time:
-
Permalink:
bartosz121/fflgs@d4f7c5b68914e130e33bbe80fadbff8261490c80 -
Branch / Tag:
refs/tags/v0.0.0a1 - Owner: https://github.com/bartosz121
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@d4f7c5b68914e130e33bbe80fadbff8261490c80 -
Trigger Event:
push
-
Statement type: