Feature Flags Extra Light - A lightweight, file-based feature flag system with gradual rollout support
Project description
FFXL-P: Feature Flags Extra Light - Python
A lightweight, file-based feature flag system for Python applications. This is a Python implementation inspired by the ffxl TypeScript library.
Features
- Time-based activation - Schedule features to be enabled from a specific date/time (UTC)
- Gradual rollout - Enable features for a percentage of users (10%, 50%, 100%, etc.)
- Environment-based control - Different rollout percentages per environment
- User-specific feature flag access control
- YAML-based configuration stored locally
Frequently asked questions
Why feature flags are stored in a file and not in cloud?
Storing feature flags in a file provides simplicity, ease of use, and version control. It allows developers to manage feature flags alongside their codebase without relying on external services. This approach is ideal for small to medium-sized projects or when you want to avoid the complexity and cost of cloud-based solutions. Additionally, file-based configurations can be easily reviewed, audited, and rolled back using standard version control systems like Git.
I need to deploy to control feature flag?
Mostly yes, aim to keep deployment process fast and simple. Or you could use gradual rollout and timebased activation to minimize the need for frequent deployments.
Installation
Install from PyPI:
pip install ffxl-p
uv add ffxl-p
Quick Start
1. Create a configuration file
Create feature-flags.yaml:
features:
new_dashboard:
enabled: true
comment: "New dashboard UI"
beta_feature:
enabled: false
comment: "Beta feature - not ready yet"
admin_panel:
onlyForUserIds:
- "user-123"
- "user-456"
comment: "Admin panel - restricted access"
allow_mock_payments:
enabled: true
environments: ["dev"]
comment: "Mock payments only in development"
experimental_feature:
rollout:
dev: 100
production: 5
comment: "Cautious rollout - 5% in production"
2. Use in your code
from ffxl_p import load_feature_flags, is_feature_enabled, User
# Load configuration
load_feature_flags(environment='production') # or get name of your environment from env variables
# Check global feature
if is_feature_enabled('new_dashboard'):
print("Show new dashboard")
# Check user-specific feature
user = "user-123"
if is_feature_enabled('admin_panel', user):
print("Show admin panel")
# Or use a dict
if is_feature_enabled('admin_panel', 'user-456'):
print("Show admin panel")
Configuration Format
Global Enabled/Disabled
If feature is disabled then its is off regardless other options. It's global parameter.
features:
feature_name:
enabled: true # or false
comment: "Optional description"
User-Specific Access
features:
feature_name:
onlyForUserIds:
- "user-1"
- "user-2"
comment: "Only for specific users"
When onlyForUserIds is present and populated, it takes precedence over the enabled setting.
Environment-Based Access
Control which environments a feature is enabled in:
features:
debug_mode:
enabled: true
environments: ["dev"]
comment: "Only in development"
staging_feature:
enabled: true
environments: ["dev", "staging"]
comment: "Dev and staging only"
production_feature:
enabled: true
environments: ["production"]
comment: "Production only"
Time-Based Activation
Control when features are enabled using start and/or end dates (UTC):
features:
# Feature enabled from a specific date
upcoming_feature:
enabled: true
enabledFrom: "2025-12-01T00:00:00Z"
comment: "Will be enabled on December 1st, 2025"
# Feature enabled until a specific date
limited_time_offer:
enabled: true
enabledUntil: "2025-12-31T23:59:59Z"
comment: "Holiday promotion ending December 31st"
# Feature with a time window (both start and end)
beta_testing:
enabled: true
enabledFrom: "2025-11-15T00:00:00Z"
enabledUntil: "2025-12-15T00:00:00Z"
comment: "One-month beta testing period"
# Combine with environment restrictions
scheduled_production_release:
enabled: true
enabledFrom: "2025-11-20T14:30:00+00:00"
environments: ["production"]
comment: "Production release scheduled for November 20th at 2:30 PM UTC"
How it works:
- Accepts ISO 8601 datetime strings in UTC timezone
enabledFrom: Feature is disabled if current time is before this dateenabledUntil: Feature is disabled if current time is after this date- Both can be used together to create a time window
- Supports both 'Z' suffix and '+00:00' for UTC timezone
- Time checks are evaluated before environment, rollout, and user restrictions
Example usage:
from ffxl_p import load_feature_flags, is_feature_enabled
load_feature_flags()
# Will return False before enabledFrom, True after
if is_feature_enabled('upcoming_feature'):
show_new_feature()
# Will return True before enabledUntil, False after
if is_feature_enabled('limited_time_offer'):
show_promotion()
# Will return True only within the time window
if is_feature_enabled('beta_testing'):
enable_beta_features()
Gradual Rollout (Percentage-Based)
Enable features for a percentage of users in each environment:
features:
new_feature:
rollout:
dev: 100 # 100% in dev
staging: 50 # 50% in staging
production: 10 # 10% in production
comment: "Gradual rollout - start small, expand carefully"
experimental_feature:
rollout:
dev: 100
staging: 25
production: 5
comment: "Cautious rollout - 5% in production"
How it works:
- Uses consistent hashing (SHA256) of
feature_name + user_id - Same user always gets same result for same feature
- Different features have independent distributions
- Requires user - percentage rollout only works when a user is provided
NOTE: 10% won't mean exactly 10 users out of 100 will get feature, it might be 11 or 9, but statistically over a large number of users it will approximate that.
Example usage:
from ffxl_p import load_feature_flags, is_feature_enabled, User
load_feature_flags(environment='production') # or get name of your environment from env variables
user = "user-123"
if is_feature_enabled('new_feature', user):
# This user is in the 10% rollout group
show_new_feature()
Combined Restrictions
Combine environment restrictions with percentage rollout:
features:
advanced_feature:
environments: ["staging", "production"]
rollout:
staging: 100 # All users in staging
production: 25 # 25% of users in production
comment: "Full staging test, then 25% production rollout"
Priority Order:
- Global
enabledflag check (ifenabled: false, feature is disabled regardless of other settings) - Time-based activation check (if
enabledFromand/orenabledUntilis specified) - Environment check (if
environmentsis specified) - Percentage rollout (if
rolloutis specified, requires user) - User-specific list (if
onlyForUserIdsis specified) - Global
enabledflag (defaults tofalseif not specified)
API Reference
Loading Configuration
# Load from default location (./feature-flags.yaml)
load_feature_flags()
# Load with explicit environment
load_feature_flags(environment='production')
# Load from custom path
load_feature_flags('./config/flags.yaml', environment='staging')
# Load as JSON string (for environment variables)
config_string = load_feature_flags_as_string()
Feature Checks
# Check single feature
is_feature_enabled('feature_name', user)
# Check if ANY features are enabled
is_any_feature_enabled(['feature1', 'feature2'], user)
# Check if ALL features are enabled
are_all_features_enabled(['feature1', 'feature2'], user)
Getting Features
# Get all enabled features for a user
enabled = get_enabled_features(user) # Returns: ['feature1', 'feature2']
# Get multiple feature flags as dict
flags = get_feature_flags(['feature1', 'feature2'], user)
# Returns: {'feature1': True, 'feature2': False}
Utility Functions
# Check if feature exists
feature_exists('feature_name')
# Get all feature names
get_all_feature_names()
# Get feature configuration
get_feature_config('feature_name')
Environment Variables
Custom Configuration File
# Option 1
export FFXL_FILE=./config/flags.yaml
# Option 2
export FEATURE_FLAGS_FILE=./config/flags.yaml
Configuration via Environment
# Pass configuration as JSON string
export FFXL_CONFIG='{"features": {"feature1": {"enabled": true}}}'
Set Current Environment
# Option 1: Use FFXL_ENV
export FFXL_ENV=production
# Option 2: Use generic ENV variable
export ENV=staging
# In code (takes precedence over env variables)
load_feature_flags(environment=os.getenv('ENV', 'local'))
Development Mode
Enable detailed logging:
export FFXL_DEV_MODE=true
Framework Integration
Flask
from flask import Flask
from ffxl_p import load_feature_flags, is_feature_enabled
import os
app = Flask(__name__)
# Load at startup
load_feature_flags()
@app.route('/')
def index():
user = session.get('user_id')
if is_feature_enabled('new_ui', user):
return render_template('new_index.html')
return render_template('old_index.html')
Django
# settings.py
from ffxl_p import load_feature_flags
load_feature_flags('./feature-flags.yaml')
# In views or middleware
from django.conf import settings
from ffxl_p import is_feature_enabled
def my_view(request):
if is_feature_enabled('new_feature', request.user.id):
# Use new feature
pass
FastAPI
from fastapi import FastAPI, Depends
from ffxl_p import load_feature_flags, is_feature_enabled
app = FastAPI()
# Load at startup
@app.on_event("startup")
async def startup_event():
load_feature_flags()
@app.get("/dashboard")
async def dashboard(user_id: str): # it's just an example, validate provided user_id properly in real code
if is_feature_enabled('new_dashboard', user_id):
return {"version": "new"}
return {"version": "old"}
Examples
See example.pyexample.py, example_environments.py and example_rollout.py for comprehensive usage examples
Development
Run with development mode for detailed logging:
FFXL_DEV_MODE=true python example.py
License
This is a Python port inspired by ffxl. Please check the original repository for license information.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
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 ffxl_p-0.1.0a1.tar.gz.
File metadata
- Download URL: ffxl_p-0.1.0a1.tar.gz
- Upload date:
- Size: 63.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 |
4d4347b1ed685887f5cdcf515d2b0830397fae6b691a164eea705cd06ffdd39f
|
|
| MD5 |
1d270598f395571b532f27bec75a30fe
|
|
| BLAKE2b-256 |
de644d4a5b7b68a19272940663f0adb74496104ff4f8b43e7b97879d17228252
|
Provenance
The following attestation bundles were made for ffxl_p-0.1.0a1.tar.gz:
Publisher:
publish.yml on emilskra/ffxl-p
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ffxl_p-0.1.0a1.tar.gz -
Subject digest:
4d4347b1ed685887f5cdcf515d2b0830397fae6b691a164eea705cd06ffdd39f - Sigstore transparency entry: 651801358
- Sigstore integration time:
-
Permalink:
emilskra/ffxl-p@93e88dd9f8849b69ea633bd9f63e6cdab7582845 -
Branch / Tag:
refs/tags/0.1.0a1 - Owner: https://github.com/emilskra
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@93e88dd9f8849b69ea633bd9f63e6cdab7582845 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ffxl_p-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: ffxl_p-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 15.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 |
ff4f68b74a5864def53b67292ed472273b8e04b68ffca3fdf6cd037f5385ec3a
|
|
| MD5 |
5f4db44f744af3453bd7d8511f761524
|
|
| BLAKE2b-256 |
75bf89410622533cd29d3c8a0489a9ad198e8ea6d5e399eacecbb87a4d883754
|
Provenance
The following attestation bundles were made for ffxl_p-0.1.0a1-py3-none-any.whl:
Publisher:
publish.yml on emilskra/ffxl-p
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ffxl_p-0.1.0a1-py3-none-any.whl -
Subject digest:
ff4f68b74a5864def53b67292ed472273b8e04b68ffca3fdf6cd037f5385ec3a - Sigstore transparency entry: 651801373
- Sigstore integration time:
-
Permalink:
emilskra/ffxl-p@93e88dd9f8849b69ea633bd9f63e6cdab7582845 -
Branch / Tag:
refs/tags/0.1.0a1 - Owner: https://github.com/emilskra
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@93e88dd9f8849b69ea633bd9f63e6cdab7582845 -
Trigger Event:
release
-
Statement type: