Manage application settings with ease using runenv, a lightweight tool inspired by The Twelve-Factor App methodology for configuration through environment variables.
runenv provides:
- A CLI for language-agnostic
.envprofile execution - A Python API for programmatic
.envloading
“Store config in the environment” — 12factor.net/config
| Section | Status |
|---|---|
| CI/CD | |
| PyPI | |
| Python | |
| Style | |
| License | |
| Docs | CHANGELOG.md |
- Key Features
- Quick Start
- Multiple Profiles
- Framework Integrations
- Parsing Behaviour
- Sample
.envFile - Similar Tools
- 🚀 CLI-First: Use
.envfiles across any language or platform. - 🐍 Python-native API: Load and transform environment settings inside Python.
- ⚙️ Multiple Profiles: Switch easily between
.env.dev,.env.prod, etc. - ⚙️ Multiple Formats: Use plain
.env,.env.json,.env.toml, or.env.yaml - ⚙️ Autodetect Env File: Looking for
.env,.env.json,.env.toml, and.env.yaml - 🔧 Parameter Expansion: Bash-style
${VAR:-default},${VAR:?msg},${VAR:+alt}operators. - ✏️ Escape Sequences:
\n,\t,\\,\"in double-quoted values;$$for a literal$. - 📄 Multi-line Values: Triple-quoted heredoc syntax (
"""..."""/'''...'''). - 🔒 Required Variables:
# @required VARdeclarations fail fast on missing config. - 📎 File Includes:
# @include pathmerges another env file inline for layered config. - 🧩 Framework-Friendly: Works well with Django, Flask, FastAPI, and more.
pip install runenv
pip install runenv[toml] # if you want to use .env.toml in python < 3.11
pip install runenv[yaml] # if you want to use .env.yamlRun any command with a specified environment:
runenv run --env-file .env.dev -- python manage.py runserver
runenv run --env-file .env.prod -- uvicorn app:app --host 0.0.0.0
runenv list [--env-file .env] # view parsed variables
runenv lint [--env-file .env] # check for errors in env file
runenv lint --strict [--env-file .env] # also warn on ambient/undefined refsNote: The
load_envwill not parse env_file if therunenvCLI was used, unless youforce=Trueit.
fromrunenvimportload_envload_env() # loads .envload_env(
env_file=".env.dev", # file to load - will be autodetected if not passedprefix='APP_', # load only APP_.* variables from filestrip_prefix=True, # strip ^ prefix when loading variablesforce=True, # load env_file even if the `runvenv` CLI was usedsearch_parent=1, # look for env_file in current dir and its 1 parent dirsrequire_env_file=False# raise error if env file is missing, otherwise just ignore
)fromrunenvimportcreate_envconfig=create_env() # parse .env content into dictionaryconfig=create_env(
env_file=".env.dev", # file to load - will be autodetected if not passedprefix='APP_', # parse only APP_.* variables from filestrip_prefix=True, # strip ^ prefix when parsing variablessearch_parent=1, # look for env_file in current dir and its 1 parent dirs
)
print(config)Options include:
- Filtering by prefix
- Automatic prefix stripping
- Searching parent directories
Use separate .env files per environment:
runenv .env.dev flask run
runenv .env.staging python main.py
runenv .env.production uvicorn app.main:appRecommended structure:
.env.dev
.env.test
.env.staging
.env.production
Note: If you're using
runenv .env [./manage.py, ...]CLI then you do not need change your code. Use these integrations only if you're using Python API.
# manage.py or wsgi.pyfromrunenvimportload_envload_env(".env")fromflaskimportFlaskfromrunenvimportload_envload_env(".env")
app=Flask(__name__)fromfastapiimportFastAPIfromrunenvimportload_envload_env(".env")
app=FastAPI()| Situation | Behaviour |
|---|---|
| Duplicate key | Last definition wins; a warning is emitted by lint |
Key exactly equal to --prefix | Skipped (stripping would produce an empty name) |
| Key without matching prefix | Skipped and reported as info by lint |
Duplicate keys are not an error — the last value in the file takes effect, matching the behaviour of most shell .env loaders. Use runenv lint to surface duplicates as warnings before they reach production.
${VAR} references resolve against variables defined in the same file and then fall back to the calling shell's os.environ. The following bash-style parameter expansion operators are supported:
| Syntax | Behaviour |
|---|---|
${VAR} | Value of VAR; empty string if unset |
${VAR:-default} | Value of VAR if set and non-empty, otherwise default |
${VAR-default} | Value of VAR if set (even if empty), otherwise default |
${VAR:?msg} | Value of VAR if set and non-empty; fatal error with msg otherwise |
${VAR:+alt} | alt if VAR is set and non-empty, otherwise empty string |
The :? operator causes runenv run / runenv list to exit non-zero and runenv lint to report an error-level message with the line number where the variable is declared.
| Style | Escape processing | Variable expansion |
|---|---|---|
Unquoted VAR=value | None | Yes |
Single-quoted VAR='value' | None | Yes |
Double-quoted VAR="value" | \n\t\r\\\" | Yes |
Triple double-quoted VAR="""...""" | Same as double-quoted | Yes |
Triple single-quoted VAR='''...''' | None | Yes |
Double-quoted values process the standard escape sequences:
GREETING="Hello\tWorld\n"# tab + newlinePATH_VAL="C:\\Users\\name"# literal backslashesQUOTED="say \"hi\""# embedded double quoteUse $$ anywhere to emit a literal $ without triggering variable expansion:
PGSERVICE=$$HOME/.pgservice # value: $HOME/.pgserviceTEMPLATE=price: $$${AMOUNT} # literal $ followed by expanded AMOUNTTriple-quoted values span multiple lines — useful for certificates, JSON blobs, or any multi-line secret:
PRIVATE_KEY="""-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA...-----END RSA PRIVATE KEY-----"""RAW_TEXT='''no \n escape processing here$$HOME is literal too'''Comments start with #. The rules depend on quoting:
| Style | # treatment |
|---|---|
Unquoted VAR=value # comment | # ends the value; trailing spaces before # are stripped |
Double-quoted VAR="value # hash" | # inside quotes is a literal character |
Single-quoted VAR='value # hash' | # inside quotes is a literal character |
DEBUG=1 # this comment is stripped → value is "1"MSG=hello world # this too → value is "hello world"TAG="v1.0 # rc"# hash is part of the value → "v1.0 # rc"To include a literal # in an unquoted value, quote the value instead.
Use # @include path to load another env file at that point in the current file. Paths are relative to the file containing the directive.
# @include .env.base# @include ../shared/secrets.envPORT=8080 # overrides anything in included files aboveMerge order: variables are processed in the order they appear — included files are expanded inline at the directive's position. A variable defined after the @include line overrides a same-named variable from the included file; a variable defined before is overridden by the included file.
Error cases reported by runenv lint:
- Included file not found —
error-level message, parsing continues - Circular include (A includes B includes A) —
error-level message, the cycle is broken
# @required directives inside included files are honoured.
Declare variables that must be present and non-empty with # @required:
# @required DATABASE_URL, SECRET_KEY# @required PORTDATABASE_URL=postgresql://localhost/mydb
SECRET_KEY=${APP_SECRET:?APP_SECRET must be set}
PORT=${PORT:-8000}If any declared variable is missing or empty after full expansion, runenv lint reports an error at the directive's line number and runenv run exits non-zero. Multiple names can appear on one line (comma-separated) or across multiple directives.
# @required and ${VAR:?msg} are complementary, not duplicates. Use # @required to declare top-level contracts on the keys your application needs. Use ${SOURCE:?msg} when building a value from another variable and you want a specific error message that names the source. Don't combine both on the same variable.
# Pull in shared base configuration# @include .env.base# Declare required variables — runenv fails fast if any are missing# @required DATABASE_URL, SECRET_KEY# export keyword accepted for shell-source compatibility
export HOST=localhost
PORT=${PORT:-8000}
URL=http://${HOST}:${PORT}
# Parameter expansionCACHE_URL=${REDIS_URL:-redis://localhost:6379} # default if unset/emptyLOG_LEVEL=${LOG_LEVEL-info} # default only if unsetFEATURE_HEADER=${FEATURE_FLAG:+X-Feature: on} # set only when flag is on# :? is for inline interpolation guards (different from # @required):# it fails with a custom message pointing at the *source* variableDATABASE_URL=${DATABASE_URL:?DATABASE_URL must be set}
SECRET_KEY=${SECRET_KEY:?SECRET_KEY must be set}
# Escape sequences in double-quoted stringsGREETING="Hello\tWorld"WINDOWS_PATH="C:\\Users\\deploy"# Literal $ with $$ — no variable expansion triggeredPGSERVICE=$$HOME/.pgservice
# Multi-line heredoc value (triple-quoted)BANNER="""Welcome to MyAppRunning on ${HOST}:${PORT}"""# Quotes and inline commentsEMAIL="admin@example.com"# Inline commentTOKEN='s3cr3t'DEBUG=1- python-dotenv – Python-focused, lacks CLI tool
- envdir – Directory-based env manager
- dotenv-linter – Linter for
.envfiles
With runenv, you get portable, scalable, and explicit configuration management that aligns with modern deployment standards. Ideal for CLI usage, Python projects, and multi-environment pipelines.