Environment Variables (.env)
Environment variables are the standard way to configure apps.
Examples:
SECRET_KEYDATABASE_URLMAIL_USERNAMEMAIL_PASSWORD
Why env vars?
Section titled “Why env vars?”- keeps secrets out of code
- same container/code can run in dev/staging/prod
Local .env files
Section titled “Local .env files”In local development, you can use a .env file.
Common library:
python-dotenv
Install:
pip install python-dotenvThen Flask can load .env automatically when using flask run (depending on setup), or you can load manually.
Do not commit .env
Section titled “Do not commit .env”Add .env to .gitignore.
Instead commit:
.env.example
So people know what variables are required.
Accessing env vars in Python
Section titled “Accessing env vars in Python”import os
secret = os.environ.get("SECRET_KEY")Always define safe defaults for development, but never for production secrets.
What load_dotenv() does, and what it refuses to do
Section titled “What load_dotenv() does, and what it refuses to do” flowchart TD
S["load_dotenv()"] --> R["read .env line by line"]
R --> Q{"is the name already in os.environ?"}
Q -->|"yes"| K["LEAVE IT ALONE
the real environment wins"]
Q -->|"no"| W["set it"]
K --> O["override=True flips this"]
Measured with APP_NAME already present in the real environment:
os.environ["APP_NAME"] = "already-in-real-env"
load_dotenv()
os.environ["APP_NAME"] # 'already-in-real-env' <- the file did NOT win
os.environ["SECRET_KEY"] # 'dev-secret' <- set, because it was absent
load_dotenv(override=True)
os.environ["APP_NAME"] # 'from-dotenv'This is the behaviour you want: .env supplies local defaults, and a real
environment variable — the kind your host injects — always beats the file. It also means
a stale export in your shell can quietly shadow the file, which is the first thing to
check when a value looks wrong.
Everything is a string
Section titled “Everything is a string”os.environ["MAX"] # '25' (str, never int)
int(os.environ["MAX"]) + 1 # 26
os.environ["EMPTY"] # '' <- empty string, not NoneWhich sets up the trap this page exists for:
.env line | bool(value) |
|---|---|
DEBUG=false | True |
DEBUG=0 | True |
DEBUG=no | True |
DEBUG=False | True |
DEBUG= | False |
Measured — every one of those is a non-empty string, and every non-empty string is
truthy. if os.environ.get("DEBUG"): turns debug mode on when you wrote DEBUG=false.
Quoting, comments and expansion
Section titled “Quoting, comments and expansion”APP_NAME=from-dotenv
QUOTED="has spaces"
EXPANDED=${APP_NAME}-suffix
# a comment
EMPTY=QUOTED 'has spaces' <- the quotes are stripped
EXPANDED 'from-dotenv-suffix' <- ${VAR} expanded from earlier lines
comments and blank lines skippedReading it in the app
Section titled “Reading it in the app”import os
from dotenv import load_dotenv
load_dotenv() # local development only
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"] # required: fail loudly
app.config["MAX_ITEMS"] = int(os.environ.get("MAX_ITEMS", 20))
app.config["DEBUG"] = env_bool("FLASK_DEBUG")os.environ[...] for anything the app cannot run without. A missing SECRET_KEY should
stop startup, not produce a confusing failure on the first login.
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
A .env file contains DEBUG=false. What does if os.environ.get('DEBUG'): evaluate to?
Measured: 'false', '0', 'no' and 'False' are all truthy. This is a real route to enabling the interactive debugger in production. Parse the value explicitly instead.
pch.quizShowAnswer
B — True, because every non-empty string is truthy — Measured: 'false', '0', 'no' and 'False' are all truthy. This is a real route to enabling the interactive debugger in production. Parse the value explicitly instead.
-
APP_NAME is already set in the real environment and also appears in .env. After load_dotenv(), which value is in os.environ?
That precedence is what makes .env safe for local defaults while a host's injected variables still win. load_dotenv(override=True) reverses it.
pch.quizShowAnswer
B — the real environment value; .env only fills in names that are absent — That precedence is what makes .env safe for local defaults while a host's injected variables still win. load_dotenv(override=True) reverses it.
-
Which belongs in version control?
A committed .env is a leaked credential that stays in history after deletion. An example file tells a new contributor what to set without exposing anything.
pch.quizShowAnswer
B — .env.example listing the keys with no values — A committed .env is a leaked credential that stays in history after deletion. An example file tells a new contributor what to set without exposing anything.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading