Skip to main content

Simple, opinionated feature flags for Django.

Project description

flagday

Simple, opinionated feature flags for Django.

PyPI version Python versions Django versions CI License: MIT

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:

  1. 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).
  2. 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 on type(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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flagday-1.0.0.tar.gz (15.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flagday-1.0.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

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

Hashes for flagday-1.0.0.tar.gz
Algorithm Hash digest
SHA256 1205e04a72be466d5956135efb7bca123b49ed707410df2c7e570b58010729a1
MD5 24cc16da60ba9db98e754ceb8366df85
BLAKE2b-256 2ad147a67742fd4e03b08c0e98cd31ed909faecc24b0f6d7f1360e1a30b99a39

See more details on using hashes here.

Provenance

The following attestation bundles were made for flagday-1.0.0.tar.gz:

Publisher: release.yml on Xof/flagday

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for flagday-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 961b4225a3ff0f38839a91fd4336bed0a1846a3ffcdf5172487e318ca7b900d6
MD5 8c7719313c8bab0bd1c14290d6a86dcd
BLAKE2b-256 453c59319549abf2736dfd3e694de0a1bd33400ba406ff7a51c802c41c434822

See more details on using hashes here.

Provenance

The following attestation bundles were made for flagday-1.0.0-py3-none-any.whl:

Publisher: release.yml on Xof/flagday

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page