python-dotenv¶
python-dotenvlets a Python program read a.envfile and inject those values intoos.environ. It is a convenient way to keep local configuration out of source code without changing the rest of the app.
Quick Reference¶
| Task | Typical approach |
|---|---|
| Install | pip install python-dotenv |
Load a .env file |
dotenv.load_dotenv() or dotenv.find_dotenv() |
| Read a variable | os.getenv("VAR") or os.environ["VAR"] |
| Override from CLI | set the env var before running the script |
No .env file |
the call is a no-op if the file is missing (by default) |
Why Use It¶
- Keep secrets and environment-specific values out of code.
- Have a default local configuration that is easy to edit.
- Make local development closer to how production config is injected.
- Avoid committing per-machine settings to version control.
A .env file is not a production secret store by itself. In production, prefer proper secret management. python-dotenv is mostly a local/development convenience and a bridge to the same os.environ interface used everywhere else.
Install¶
Minimal Example¶
.env:
Python:
import os
from dotenv import load_dotenv
load_dotenv()
database_url = os.getenv("DATABASE_URL")
log_level = os.getenv("LOG_LEVEL", "INFO")
timeout = int(os.getenv("API_TIMEOUT", "30"))
load_dotenv() by default looks for a .env file in the current working directory.
Loading Patterns¶
Default location¶
Loads .env from the current directory if present.
Explicit path¶
Use this when the file is in a known non-current location.
Find the project root¶
A common pattern for repos with deeper layouts:
find_dotenv() searches upward for a .env file. This helps when the script is run from a subdirectory.
Load once, early¶
Load environment variables near startup, before configuration is parsed. Loading late can create a confusing gap where some code uses .env values and some does not.
Variable Precedence¶
Environment variables already set in the process take precedence over .env values by default.
That means:
- .env is a convenient default.
- Real env vars can override them.
- You can run with DATABASE_URL=... python app.py and bypass the file.
This is usually what you want: the file sets defaults, and real environment variables win in production or in CI.
If you want .env to overwrite existing values, use override=True:
Use that deliberately. It can be useful for tests or local resets, but it can also hide the fact that the real environment is not behaving as expected.
Common Syntax¶
KEY=value
SECRET_KEY=abcdef123456
PORT=8000
DEBUG=false
MESSAGE=hello world
MULTILINE="line one
line two"
EXPORT_NOT_USED=1
Notes:
- No quoting is required for simple values.
- Quoting can help with spaces or special characters.
- Lines starting with # are comments.
- Empty lines are ignored.
- export prefixes are not used by python-dotenv itself in the normal loading model; they are just text in the file.
Multiline Values¶
OPENAI_SYSTEM_PROMPT="You are a concise assistant.
Keep answers short.
Do not explain unless asked."
Multiline values are supported when quoted. Use them for templates, prompts, or small configuration blobs that are easier to keep in a file than inline in code.
Keep in mind: - Indentation and newlines become part of the value. - The exact result depends on quoting style. - Large configuration blobs may be better stored elsewhere.
Interpolation¶
Some .env setups support variable interpolation, but python-dotenv's behavior is intentionally simple. If you need a value to reference another variable, compute it in Python instead of relying on fragile file-time substitution.
Example in Python:
import os
from dotenv import load_dotenv
load_dotenv()
host = os.getenv("DB_HOST", "localhost")
port = os.getenv("DB_PORT", "5432")
database_url = f"postgresql://{host}:{port}/mydb"
This is clearer and less error-prone than expecting a .env file to do substitution.
Parsing Values¶
.env values are strings. Convert them explicitly.
import os
from dotenv import load_dotenv
load_dotenv()
debug = os.getenv("DEBUG", "false").strip().lower() in {"1", "true", "yes"}
port = int(os.getenv("PORT", "8000"))
rate_limit = float(os.getenv("RATE_LIMIT", "100"))
Do not assume "false" or "0" will be falsy automatically. Explicit parsing avoids a whole class of configuration bugs.
Using with argparse or config objects¶
A common pattern is to read env vars as defaults and then let CLI arguments override them:
import argparse
import os
from dotenv import load_dotenv
load_dotenv()
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000")))
parser.add_argument("--debug", action="store_true", default=os.getenv("DEBUG", "false").lower() == "true")
args = parser.parse_args()
This gives a clear precedence chain:
1. .env provides the default.
2. CLI flags override it.
3. Production environments set the real env vars directly.
No .env File in Production¶
A .env file is usually not the right production mechanism. Typical production setups:
- Inject variables directly into the environment.
- Use secret managers.
- Use orchestration config.
- Mount config files where the app reads them.
python-dotenv should not be relied on as the only production configuration path. Structure your app so it reads from os.environ either way, and treat .env as the local file-based fallback.
Security Considerations¶
- Do not commit real secrets to
.envif it is tracked in version control. - Add
.envto.gitignoreunless you intentionally ship a template. - Keep a
.env.examplewith placeholder values for collaborators. - Do not assume
.envprovides real security boundaries. - Rotate secrets and avoid reusing them across environments.
A typical repo layout:
Testing and Dotenv¶
For tests, you can:
- Load a test .env file.
- Override variables with override=True.
- Set environment variables before the test run.
- Isolate test configuration from local development configuration.
Avoid tests that depend on the user's personal .env file. That makes tests brittle and hard to share.
Example:
def test_with_fake_env(monkeypatch):
monkeypatch.setenv("API_TIMEOUT", "5")
assert api_timeout() == 5
When a library or module already called load_dotenv() at import time, patching os.environ may still be the cleanest way to control behavior in a test.
Common Mistakes¶
- Expecting
.envto work in production without real env injection. - Committing secrets into
.envand then into git. - Parsing booleans and numbers implicitly instead of explicitly.
- Loading
.envin the middle of the program instead of early. - Using
override=Truewithout understanding that it hides the real environment. - Putting large config blobs in
.envinstead of a proper config file. - Relying on interpolation that the library does not actually provide.
- Having different
.envconventions across developers and never reconciling them.
Related Cheat Sheets¶
- yaml — structured config files
- toml — another common config format
- make — orchestrating commands including env-aware runs
- curl — testing endpoints that may depend on env-derived config
- ssh — remote environments where env vars are often set directly
- fastapi — configuration patterns in a common Python service framework
Official Documentation¶
- python-dotenv on PyPI
- python-dotenv GitHub repository
os.environandos.getenvin the Python standard library
The library is small. The bigger win is using it as part of a clear configuration story rather than as a replacement for one.
Last Updated¶
2026-09-06 — Initial version.