Feature flags with pydantic settings
A feature flag is just configuration, so it belongs in the same validated settings object as everything else — not in a loose os.environ.get scattered through the code. This page models flags as typed fields, extending Multi-Environment Settings Override.
Problem 1: flags read as raw strings
# ANTI-PATTERN: the bool("false") trap, per flag, everywhere
if os.environ.get("FEATURE_NEW_CHECKOUT"): # "false" is truthy -> always on
use_new_checkout()
Reading flags as raw strings reintroduces the truthiness bug for every flag.
Problem 2: flags defaulting to on
# ANTI-PATTERN: missing variable enables an unfinished feature
ENABLE_BETA = os.environ.get("ENABLE_BETA", "true") # on unless explicitly disabled
A flag should be off unless explicitly enabled; defaulting to on ships unfinished work.
Secure implementation
# config/flags.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class FeatureFlags(BaseSettings):
model_config = SettingsConfigDict(env_prefix="FEATURE_", extra="forbid")
new_checkout: bool = False # FEATURE_NEW_CHECKOUT; off by default
async_emails: bool = False # FEATURE_ASYNC_EMAILS
beta_dashboard: bool = False # FEATURE_BETA_DASHBOARD
flags = FeatureFlags() # "false"/"0"/"no" parsed correctly to False
if flags.new_checkout: # typed bool, validated at startup
use_new_checkout()
env_prefix="FEATURE_" groups the flags, every flag is a typed bool defaulting to False, and pydantic parses "false"/"0"/"no" correctly. A flag is never an unparsed string.
Gotchas & version-specific behaviour
- pydantic parses bool strings properly; never wrap a flag in
bool(). - Default every flag to
Falseso a missing variable disables, not enables. extra="forbid"with a prefix catches a typo’d flag name at startup.- For per-environment rollout, flip the variable via the environment overlay, not code.
Production parity checklist
- Flags are typed boolean fields on a settings object, not loose env reads.
- Every flag defaults to
False. - Flags share a prefix and
extra="forbid". - Per-environment values come from injected variables, not code branches.
- Removing a flag follows the schema evolution process.
Conclusion
Model flags as defaulted boolean fields and they validate, document themselves, and never fall into the truthiness trap. Flip them per environment through overrides — see Override Pydantic Settings per Environment.