Simple, opinionated feature flags for Django.
Project description
flagday
Simple, opinionated feature flags for Django.
Why flagday?
flagday gives you dynamic, database-backed feature flags for Django with a deliberately tiny surface: read a flag with flag.something, set it with flag.something = value. No third-party SaaS, no gRPC, no per-request fan-out — just a Django model and a thin attribute-style accessor.
flagday is stateless by design: every read does its own DB query. That means changes are visible immediately to every process without cache invalidation logic, and it means flagday is wrong for hot paths that read the same flag thousands of times per request. If you need caching, wrap flag in your own thin layer — flagday will not grow one in v1.
Installation
pip install flagday
Add "flagday" to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"flagday",
]
Run migrations:
python manage.py migrate flagday
Quick start
from flagday import flag
# Read — returns the stored string, or False (default) if the flag doesn't exist.
if flag.maintenance_mode:
show_maintenance_banner()
# Set — upserts the row.
flag.maintenance_mode = True
flag.api_rate_limit = "100"
Reading flags
flag.<name> issues one DB lookup per call. There is no caching layer. Reads are synchronous (a blocking ORM query); offload them to a worker thread (e.g. asgiref.sync.sync_to_async) if you need a flag inside an async view.
When the flag does not exist, the return value is governed by the
FLAGDAY_MISSING_FLAG_HANDLING setting:
| Mode | Behavior |
|---|---|
"return_false" (default) |
returns False |
"return_empty" |
returns "" |
"return_none" |
returns None |
"exception" |
raises FlagdayNoSuchFlagException |
Reading a name that begins with _ (or a dunder) raises AttributeError without hitting the database, so Python's own attribute probes never turn into flag queries. A name that is an actual method of the singleton (e.g. set_from_json) resolves to that method on read — so it can't be read as a flag either. The reserved-name rule that rejects such names raises on writes and on set_from_json keys; see Reserved names.
Setting flags
flag.<name> = value upserts the underlying row. Python values are translated to storage strings as follows:
| Python value | Stored value |
|---|---|
False |
"" (empty string) |
True |
"True" |
None |
NULL |
| anything else | str(value) |
The False → "" mapping is what lets adopters distinguish missing (NULL) from false (empty string) when introspecting the database directly. All reads return strings (or one of the missing-flag sentinels listed above), so adopters parse on the read side if they need a richer type.
flag.maintenance = True # stored as "True"
flag.feature_x = False # stored as ""
flag.cleared = None # stored as NULL
flag.threshold = 42 # stored as "42"
Assigning to a reserved name raises:
flag._private = "x" # AttributeError: '_private' is reserved...
flag.set_from_json = "x" # AttributeError: 'set_from_json' is reserved...
Flag names are limited to 255 characters (the model's name column); assigning a longer name raises ValueError before any write.
Bulk loading from JSON
flag.set_from_json(blob) accepts a JSON object or a list of objects, and upserts every key inside a single transaction (so a late failure rolls back earlier writes).
flag.set_from_json('{"feature_a": true, "feature_b": "experiment-2"}')
flag.set_from_json('[{"feature_a": true}, {"feature_b": "experiment-2"}]')
The blob must be str, bytes, or bytearray. Values must be JSON scalars (string, number, bool, null); nested objects and arrays raise FlagdayInvalidJSONException. Keys longer than 255 characters raise FlagdayInvalidJSONException as well.
Configuration
Only one setting:
# settings.py
FLAGDAY_MISSING_FLAG_HANDLING = "return_false" # default
Valid values: "return_false", "return_empty", "return_none", "exception". Anything else raises ImproperlyConfigured the next time a missing flag is read.
Templates
Add the context processor:
TEMPLATES = [
{
# ...
"OPTIONS": {
"context_processors": [
# ...
"flagday.context_processors.flag",
],
},
}
]
Then in templates:
{% if flag.maintenance_mode %}
<p>The site is under maintenance.</p>
{% endif %}
Reserved names
flagday rejects two classes of flag names:
- Underscore-prefixed names (
_foo,__bar__). These short-circuit at the attribute layer to avoid being confused with Python's internal protocol attributes (dunders, name-mangled attributes). - Names that match an actual method on the singleton. Today the only such method is
set_from_json, but the rule is general: if the name resolves ontype(flag), it cannot be used as a flag name.
Reserved names raise AttributeError when assigned via flag.<name> = .... When supplied as a key to set_from_json, they raise FlagdayInvalidJSONException instead.
Exceptions
from flagday import (
FlagdayException, # base class for everything below
FlagdayNoSuchFlagException, # raised by flag.<name> when missing + mode="exception"
FlagdayInvalidJSONException, # raised by flag.set_from_json on bad input
)
FlagdayNoSuchFlagException exposes the missing flag name as .flag_name.
Versioning & compatibility
flagday follows Semantic Versioning. The 1.0.0 release is Production/Stable; the public API is from flagday import flag, Flag, FlagdayException, FlagdayNoSuchFlagException, FlagdayInvalidJSONException and the FLAGDAY_MISSING_FLAG_HANDLING setting.
Supported runtimes:
- Python: 3.10, 3.11, 3.12, 3.13
- Django: 4.2 LTS, 5.1, 5.2, 6.0
Both declarations are open-ended: Django>=4.2 and requires-python = ">=3.10" have no upper bound. The versions listed above are the ones exercised in CI; newer Python and Django releases are presumed compatible until proven otherwise.
Development
git clone https://github.com/Xof/flagday.git
cd flagday
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # run tests
ruff check . # lint
ruff format --check . # format check
mypy src tests # type-check
For contributors: ARCHITECTURE.md is the component map, invariants, and landmines (the fast cold-start reference); THEORY.md is the design rationale — why flagday is stateless, the NULL/empty-string distinction, and the reserved-name machinery.
License
MIT — see LICENSE.
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 flagday-1.0.0.tar.gz.
File metadata
- Download URL: flagday-1.0.0.tar.gz
- Upload date:
- Size: 15.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1205e04a72be466d5956135efb7bca123b49ed707410df2c7e570b58010729a1
|
|
| MD5 |
24cc16da60ba9db98e754ceb8366df85
|
|
| BLAKE2b-256 |
2ad147a67742fd4e03b08c0e98cd31ed909faecc24b0f6d7f1360e1a30b99a39
|
Provenance
The following attestation bundles were made for flagday-1.0.0.tar.gz:
Publisher:
release.yml on Xof/flagday
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flagday-1.0.0.tar.gz -
Subject digest:
1205e04a72be466d5956135efb7bca123b49ed707410df2c7e570b58010729a1 - Sigstore transparency entry: 1902023987
- Sigstore integration time:
-
Permalink:
Xof/flagday@c080087f04db22d355e8f6013e2ae107827ba6fe -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Xof
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c080087f04db22d355e8f6013e2ae107827ba6fe -
Trigger Event:
release
-
Statement type:
File details
Details for the file flagday-1.0.0-py3-none-any.whl.
File metadata
- Download URL: flagday-1.0.0-py3-none-any.whl
- Upload date:
- Size: 12.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
961b4225a3ff0f38839a91fd4336bed0a1846a3ffcdf5172487e318ca7b900d6
|
|
| MD5 |
8c7719313c8bab0bd1c14290d6a86dcd
|
|
| BLAKE2b-256 |
453c59319549abf2736dfd3e694de0a1bd33400ba406ff7a51c802c41c434822
|
Provenance
The following attestation bundles were made for flagday-1.0.0-py3-none-any.whl:
Publisher:
release.yml on Xof/flagday
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flagday-1.0.0-py3-none-any.whl -
Subject digest:
961b4225a3ff0f38839a91fd4336bed0a1846a3ffcdf5172487e318ca7b900d6 - Sigstore transparency entry: 1902024087
- Sigstore integration time:
-
Permalink:
Xof/flagday@c080087f04db22d355e8f6013e2ae107827ba6fe -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Xof
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c080087f04db22d355e8f6013e2ae107827ba6fe -
Trigger Event:
release
-
Statement type: