Feature Flag Framework for Django.
python -m pip install django-featurevault
First, add django_featurevault to your INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"django_featurevault",
]Next, add FeatureContextMiddleware to your MIDDLEWARE list:
MIDDLEWARE = [
# ...
"django_featurevault.middleware.FeatureContextMiddleware",
]Finally, configure your feature flags in settings.py:
FEATURE_FLAGS = {
"default": {
"BACKEND": "django_featurevault.backends.settings.SettingsBackend",
"OPTIONS": {
"FLAGS": {
"GLOBAL_BANNER": True,
"STAFF_DASHBOARD": {
"enabled": True,
"conditions": {
"groups": [
{
"properties": [
{"key": "is_staff", "operator": "exact", "value": True}
]
}
]
},
},
"NEW_CHECKOUT": {
"enabled": True,
"conditions": {
"groups": [
{"rollout_percentage": 50}
]
},
},
}
},
}
}Few backends are included by default:
django_featurevault.backends.settings.SettingsBackend: Reads feature flags directly fromsettings.FEATURE_FLAGS.django_featurevault.backends.dummy.DummyBackend: In-memory backend for testing.
You can create custom storage backends by subclassing BaseFeatureBackend and implementing get_feature and get_all_features:
from typing import Any
from django_featurevault.backends.base import BaseFeatureBackend
class CustomRedisBackend(BaseFeatureBackend):
def __init__(self, alias: str = "default", **options: Any) -> None:
super().__init__(alias=alias, **options)
# Initialize client connections using options passed from settings
self.redis_url = options.get("URL", "redis://localhost:6379/0")
def get_feature(self, feature_name: str, default: Any = False) -> dict[str, Any]:
"""
Fetch feature configuration dictionary by name.
Must return a dict containing at minimum:
{"enabled": bool, "conditions": dict}
"""
# Fetch from your storage engine...
return {
"enabled": True,
"conditions": {},
}
def get_all_features(self) -> dict[str, dict[str, Any]]:
"""Fetch all feature definitions for bulk evaluation."""
return {}Point to your custom backend in settings.py:
FEATURE_FLAGS = {
"default": {
"BACKEND": "my_app.backends.CustomRedisBackend",
"OPTIONS": {
"URL": "redis://127.0.0.1:6379/1",
},
}
}Import feature and call is_enabled:
from django.shortcuts import render
from django_featurevault import feature
def home_view(request):
if feature.is_enabled("NEW_CHECKOUT"):
return render(request, "new_checkout.html")
return render(request, "old_checkout.html")If a flag is not defined, is_enabled returns False by default. You can change this using the default parameter:
feature.is_enabled("UNKNOWN_FLAG", default=True)You can pass a custom context dictionary directly into is_enabled:
feature.is_enabled("BETA_FEATURE", context={"plan": "enterprise", "country": "IN"})To evaluate feature flags in Celery workers, cron jobs, or tests where no HTTP request exists, use the context manager:
from django_featurevault import feature
with feature.context(user_id="user_101", is_staff=True):
if feature.is_enabled("STAFF_DASHBOARD"):
...Flags defined as dictionaries support targeting rules via conditions.
"FEATURE_NAME": {
"enabled": True,
"conditions": {
"groups": [
# Group 1: Enabled for internal staff
{
"properties": [
{"key": "is_staff", "operator": "exact", "value": True}
],
}
# OR Group 2: Enabled for 20% of beta users in India
{
"properties": [
{"key": "country", "operator": "exact", "value": "IN"},
{"key": "plan", "operator": "exact", "value": "beta"}
],
"rollout_percentage": 20,
}
]
},
}groups: Evaluated with OR logic (if any group matches, the flag is enabled).propertieswithin a group: Evaluated with AND logic (all properties in the group must match).rollout_percentage: A percentage between 0 and 100 that uses sticky hashing against the user or device ID.
exact: Matches exact equality (==).is_not: Matches inequality (!=).in: Checks values in a list (value in [...]).icontains: Case-insensitive substring match.
When evaluating against an authenticated Django user, the following context keys are resolved automatically:
user_id,id,pk: The user's primary key (user.pk).username: The user's username (getattr(user, user.USERNAME_FIELD)).django_group: Group names the user belongs to (user.groups.values_list("name", flat=True)).- Any standard or custom attribute on the user model (like
is_staff,is_superuser,email).
FeatureContextMiddleware automatically sets an anonymous device cookie for sticky rollouts when users are not logged in.
You can customize the cookie name and options in settings.py:
FEATURE_FLAGS = {
"CLIENT_ID_COOKIE": "ff_client_id",
"COOKIE_OPTIONS": {
"max_age": 30 * 24 * 60 * 60,
"httponly": True,
"samesite": "Lax",
},
}To expose evaluated feature flags to frontend clients as JSON, include the URLs in your urls.py:
from django.urls import include, path
urlpatterns = [
# ...
path("features/", include("django_featurevault.urls")),
]This registers GET /features/api/flags/, and returns a JSON map of all evaluated flags:
{
"GLOBAL_BANNER": {"enabled": true},
"STAFF_DASHBOARD": {"enabled": false},
"NEW_CHECKOUT": {"enabled": true}
}
An example project is included in the example/ directory.