honest-features
A feature flag is a named state that routes execution to a handler.
# Environment variable flags
import os
NEW_CHECKOUT = os.getenv("NEW_CHECKOUT", "false") == "true"
def handle_checkout(manifest):
if NEW_CHECKOUT:
return new_checkout_handler(manifest)
else:
return legacy_checkout_handler(manifest)
# Read at import time. Changing it requires a restart.
# if/else dispatch. No record of what flags exist.
from honest_features import vocabulary, feature_state
FEATURES = {
"new_checkout": {"states": {"on", "off"}, "default": "off"},
}
CHECKOUT_HANDLERS = {
"on": new_checkout_handler,
"off": legacy_checkout_handler,
}
def handle_checkout(manifest):
return CHECKOUT_HANDLERS[feature_state("new_checkout")](manifest)
# Toggle via API. No restart. No if/else. Full flag inventory.
if Flipper[:new_checkout].enabled?
new_checkout_handler(manifest)
else
legacy_checkout_handler(manifest)
end
# Database-backed flag state.
# if/else dispatch.
# The Flipper gem is a runtime dependency.
FEATURES = {
"new_checkout" => { states: %w[on off], default: "off" }
}
CHECKOUT_HANDLERS = {
"on" => method(:new_checkout_handler),
"off" => method(:legacy_checkout_handler),
}
def handle_checkout(manifest)
CHECKOUT_HANDLERS[feature_state("new_checkout")].call(manifest)
end
# Toggle via HMAC API. No restart. No if/else.
if (config('features.new_checkout')) {
return newCheckoutHandler($manifest);
} else {
return legacyCheckoutHandler($manifest);
}
// Config file flag. Restart required to change.
// if/else dispatch.
$features = [
'new_checkout' => ['states' => ['on','off'], 'default' => 'off'],
];
$checkoutHandlers = [
'on' => 'newCheckoutHandler',
'off' => 'legacyCheckoutHandler',
];
function handleCheckout($manifest) {
return $checkoutHandlers[featureState('new_checkout')]($manifest);
}
// Toggle via HMAC API. No restart. No if/else.
var newCheckout = os.Getenv("NEW_CHECKOUT") == "true"
func handleCheckout(manifest map[string]any) map[string]any {
if newCheckout {
return newCheckoutHandler(manifest)
}
return legacyCheckoutHandler(manifest)
}
// Read at startup. Restart required to change.
// if/else dispatch. No flag inventory.
var Features = map[string]Feature{
"new_checkout": {States: Set{"on","off"}, Default: "off"},
}
var CheckoutHandlers = map[string]Handler{
"on": newCheckoutHandler,
"off": legacyCheckoutHandler,
}
func handleCheckout(manifest Manifest) Manifest {
return CheckoutHandlers[FeatureState("new_checkout")](manifest)
}
// Toggle via HMAC API. No restart. No if/else.
# Flag state in environment variables or config files.
# Changing a flag requires a restart or redeploy.
# Dispatch is if/else.
# The full set of flags is implicit — read the codebase to find them.
# Testing requires environment manipulation.
FEATURES = {
"new_checkout": {"states": {"on","off"}, "default": "off"},
"pricing": {"states": {"a","b","control"}, "default": "control"},
}
# Vocabulary is the complete flag inventory.
# State is ephemeral in-memory data.
# Toggle via HMAC API — no restart, no redeploy.
# Dispatch is a handler table. Tests mutate _state directly.
The concept
You have an API endpoint. Someone calls it with ?sort=name&order=asc&page=3. At the other end of your chain, a function expects sort to be either "on" or "off", because this vocabulary was designed for a flag, and you wired the wrong vocab to this handler. The call succeeds. The wrong handler runs. No error.
This is the problem honest-features solves. A feature flag is a named state. A named state has a finite set of valid values. Code that acts on that state should only ever see one of those values — and should fail loudly, at the point of declaration, if you try to dispatch to a state that does not exist.
The vocabulary
Flags are declared once, as a plain dict:
FEATURES = {
"new_checkout": {"states": {"on", "off"}, "default": "off"},
"pricing": {"states": {"a", "b", "control"}, "default": "control"},
}
The vocabulary is the authoritative list. Add a flag here, it exists everywhere. Try to reference an undeclared flag and you get a KeyError immediately — not a silent wrong branch, not a None default, not a flag that behaves as if it is off without saying so.
The handler table
The typical pattern is if flag == "on": ... elif flag == "off": .... The problem: there is no enforcement that both branches exist. Add a third state and the if/elif chain silently takes the else path, or raises, or does nothing, depending on how it was written.
A handler table cannot do this:
CHECKOUT_HANDLERS = {
"on": new_checkout_handler,
"off": legacy_checkout_handler,
}
def handle_checkout(manifest):
return CHECKOUT_HANDLERS[feature_state("new_checkout")](manifest)
If "on" is in FEATURES["new_checkout"]["states"] but not in CHECKOUT_HANDLERS, the lookup fails immediately when that state is dispatched to — not silently, not later, right there. Add a new state to the vocabulary and every handler table that does not cover it breaks explicitly.
Toggling state
State changes via a signed API call. No restart. No redeploy. No environment variable:
POST /hf/features/set
{ "flag": "new_checkout", "state": "on", "timestamp": 1710000000, "signature": "..." }
The signature covers the full payload — flag, state, and timestamp together. This prevents replay attacks (a valid request cannot be resent with a different body) and timestamp replay (old requests expire). On process restart, flags reinitialize from vocabulary defaults. A restarted process always starts from a known declared state.
Testing
_state is a plain dict. Tests set it directly:
def test_new_checkout_on():
_state["new_checkout"] = "on"
result = handle_checkout(manifest)
assert result == expected_new
No API calls. No environment manipulation. A pytest fixture resets all flags to defaults between tests. honest-test generates every state combination automatically from the vocabulary.
The abstract principle
A feature flag is a named dimension of variation in a program's behavior. The set of valid values for that dimension is finite and declared. The current value is ephemeral runtime state.
These three sentences define the entire design space. Every implementation decision follows from them.
Dispatch as a total function
Routing behavior based on flag state is a function from state to handler:
dispatch : FlagState → Handler
For this function to be total — defined for every possible input — the handler table must contain an entry for every member of the flag's state set. A missing entry is an incomplete function definition, not a runtime edge case. honest-check HC-HF002 catches incomplete handler tables at static analysis time.
An if/else conditional on flag state is an incomplete, implicit, unverifiable representation of the same function. It cannot be checked for totality without running it. The handler table can be checked for totality by inspecting the keys: set(FEATURES[flag]["states"]) == set(HANDLER_TABLE.keys()). This is a set equality check — the same operation that honest-check performs.
Ephemeral state is not a compromise
_state — the in-memory dict — resets to vocabulary defaults on process restart. This is described in the design as a feature. It is not a compromise forced by implementation convenience. It is a formal property: every process starts from a declared, known state. The flag vocabulary is the specification of valid initial states. The defaults are the specified starting point.
If you need persistence across restarts, you persist the state externally and restore it via the toggle API after startup. The mechanism is explicit. The startup state is always declared and known. There are no hidden implicit state sources.
HMAC as intent binding
The toggle endpoint uses HMAC-SHA256 over {flag}:{state}:{timestamp}. A bearer token proves identity but not intent. An intercepted request with a valid bearer token can be replayed with a different body — different flag, different state. The HMAC signature covers the full payload: flag name, target state, and timestamp. A tampered body invalidates the signature. An intercepted and replayed request is rejected by the timestamp window.
This is the principle of intent binding: the cryptographic proof covers not just who sent the request but what they intended. The signature cannot be detached from the payload and reused for a different intention. This is the same principle that prevents SQL injection (parameterized queries bind the intent to specific values) and CSRF protection (tokens bind the intent to a specific form submission).
Why environment variables are dishonest
An environment variable is a configuration value that: - Has no declared vocabulary (any string is valid) - Has no declared set of valid states (no exhaustive list) - Changes require a process restart (not at runtime) - Cannot be toggled without infrastructure access - Emits no events when changed
FEATURES is honest about all five dimensions. The vocabulary declares valid states. Toggling requires no restart. The toggle API emits hf.features.changed to the event log. The full flag inventory is readable at module scope. Every property that makes environment variables dishonest, the vocabulary dict makes honest.
Full specification
The Flag Vocabulary Schema
FEATURES: dict[str, dict] = {
"flag_name": {
"states": set[str], # complete set of valid states, min 2 members
"default": str, # must be a member of states
},
...
}
Rules:
- states must contain at least two members
- default must be a member of states
- No other keys are permitted at the vocabulary level
- The vocabulary is declared at module scope, not constructed dynamically
- FEATURES is the canonical inventory of all flags. No flag may be referenced that is not declared here.
The _state Dict
_state: dict[str, str] = {k: v["default"] for k, v in FEATURES.items()}
_state is initialized at module import time. No I/O. No environment variables. No config files. Initialization is deterministic.
The feature_state() Contract
def feature_state(flag: str) -> str:
return _state[flag]
- Returns the current state of the named flag
- Raises
KeyErrorfor undeclared flag names — not a silent default - This is intentional: an undeclared flag reference is a programming error, not a runtime condition
The Toggle Endpoint
POST /hf/features/set
Content-Type: application/json
{
"flag": string, # must be in FEATURES
"state": string, # must be in FEATURES[flag]["states"]
"timestamp": integer, # Unix timestamp
"signature": string, # HMAC-SHA256 hex digest
}
Validation order:
1. flag in FEATURES → 400 if not
2. state in FEATURES[flag]["states"] → 400 if not
3. timestamp within replay window → 403 if outside
4. signature valid → 403 if not
Signature construction:
message = f"{flag}:{state}:{timestamp}"
signature = hmac_sha256(secret, message)
Signature verification must use hmac.compare_digest (or language equivalent constant-time comparison). String equality comparison is vulnerable to timing attacks.
Replay window: configurable, default 60 seconds. Requests with |now() - timestamp| > window are rejected with 403.
Successful response (200):
{"flag": "new_checkout", "state": "on", "previous": "off"}
The Handler Table Pattern
Every dispatch on flag state must use a handler table. if/else on feature_state() return values is an HC-P001 violation.
# Required pattern
HANDLERS: dict[str, Callable] = {
"on": handler_a,
"off": handler_b,
}
def dispatch(manifest: dict) -> dict:
return HANDLERS[feature_state("flag_name")](manifest)
Handler table completeness: the table must contain an entry for every state in the flag's vocabulary. A missing state raises KeyError at dispatch time. honest-check HC-HF002 catches incomplete handler tables statically.
honest-observe Integration
The toggle endpoint emits on every successful state change:
{
"event_type": "hf.features.changed",
"flag": "flag_name",
"previous": "old_state",
"state": "new_state",
"timestamp": 1710000000,
"requesting_ip": "10.0.0.1"
}
feature_state() emits on every call within a request context:
{
"event_type": "hf.features.evaluated",
"flag": "flag_name",
"state": "current_state",
"request_id": "req_abc123"
}
Evaluation events outside a request context (background tasks, startup) are not emitted.
Testing Contract
Tests manipulate _state directly. No API calls. No process restart. No environment manipulation.
from features import _state, FEATURES
def test_flag_on():
_state["flag_name"] = "on"
result = dispatch(manifest)
assert result == expected
# Reset fixture — must be autouse
@pytest.fixture(autouse=True)
def reset_features():
yield
for flag, spec in FEATURES.items():
_state[flag] = spec["default"]
The reset fixture must be autouse=True. Tests that do not reset _state produce ordering-dependent results, which is a honesty violation.
honest-check Rules
| Rule | Description | Severity |
|---|---|---|
| HC-HF001 | feature_state() call references undeclared flag name | Error |
| HC-HF002 | Handler table missing entry for declared flag state | Warning |
Conformance Requirements
| Requirement | Test |
|---|---|
_state initialized from FEATURES defaults at import, no I/O | Verify no network/file calls |
feature_state() raises KeyError for undeclared flag | Test with unknown flag name |
| Toggle endpoint validates flag before state | Send invalid flag, verify 400 |
| Toggle endpoint validates state against vocabulary | Send invalid state, verify 400 |
| Signature uses constant-time comparison | Code review / linter |
| Replay window rejects stale timestamps | Send timestamp 61s ago, verify 403 |
Process restart reinitializes _state from defaults | Restart, verify all flags at default |
hf.features.changed emitted on successful toggle | Toggle, check event log |
hf.features.evaluated emitted per feature_state() call in request context | Instrument and verify |
No if/else dispatch on flag state anywhere in application | honest-check HC-P001 |