.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

  • direnv requires direnv allow per .envrc — a deliberate trust step.
  • python-dotenv should be a dev/test dependency, not required to boot in production.
  • All three need .env in .gitignore; the tool choice does not change that.
  • Keep override=False semantics so injected values win regardless of tool.

Production parity checklist

  • .env is gitignored; .env.example documents 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.