.env vs python-dotenv vs direnv for local dev
Three tools claim to “load your .env”: a raw file your shell sources, python-dotenv inside the app, and direnv at the shell level. They have different blast radii and different parity stories. This page picks between them, extending .env File Management.
Problem 1: sourcing .env into your interactive shell
# ANTI-PATTERN: secrets persist in your whole shell session
set -a; source .env; set +a # every later command — and child — inherits them
Every subsequent command in that terminal, including unrelated tools, inherits the secrets.
Problem 2: app code that requires the file to exist
# ANTI-PATTERN: production has no .env, so this is fragile
from dotenv import load_dotenv
load_dotenv() # silently does nothing in prod; masks missing config
python-dotenv is great locally but must be a no-op (and a non-dependency) in production.
Secure implementation
# config/loader.py — python-dotenv, scoped to the process, override=False
import os
from pathlib import Path
from dotenv import dotenv_values
def hydrate() -> None:
for key, value in dotenv_values(Path(".env")).items():
if value and key not in os.environ: # never override injected values
os.environ[key] = value
# .envrc — direnv: auto-loads per directory, unloads when you leave it
dotenv # direnv reads .env when you cd in, clears it when you cd out
python-dotenv keeps the values inside the process; direnv scopes them to the project directory and unloads on exit — both better than sourcing into your whole shell. All three rely on .env being gitignored.
Comparison
| Tool | Scope | Loads into | Best for |
|---|---|---|---|
raw .env + source |
whole shell session | your interactive shell | nothing — too broad |
python-dotenv |
the Python process | os.environ (in-process) |
app-level local config |
direnv |
the project directory | the shell, auto-unloaded | per-repo dev environments |
Gotchas & version-specific behaviour
direnvrequiresdirenv allowper.envrc— a deliberate trust step.python-dotenvshould be a dev/test dependency, not required to boot in production.- All three need
.envin.gitignore; the tool choice does not change that. - Keep
override=Falsesemantics so injected values win regardless of tool.
Production parity checklist
.envis gitignored;.env.exampledocuments the keys.- Production injects variables via the orchestrator — no tool loads a file there.
- The app reads
os.environ; the loader only seeds missing keys. - Secrets are never sourced into the interactive shell.
Conclusion
Use python-dotenv (or direnv) to scope local secrets to the process or directory; never source them into your shell. The production path stays identical because the app only ever reads os.environ. See os.environ vs python-dotenv for the read side.