Configuring my code with “.env” files
2017-04-18 — 2025-08-20
Wherein the practice of loading local environment files is considered, and a shell extension that auto‑loads and unloads a project .envrc per directory is described, with explicit approval being required.
Assumed audience:
People who run code on multiple machines
Tips for configuring ML and other apps.
There are many tools to load environment variables from local files. A good place to find generic resources on this is the “Twelve-Factor App” guidance on configuration. But the Twelve-Factor App covers eleven other factors I don’t care about — I’m not a web developer; I only want the environment configuration part.
1 direnv
direnv
is a small shell extension that automatically loads and unloads environment variables depending on the directory we’re in. Put a file called .envrc
in our project root and direnv
will evaluate it whenever we cd
into that directory. Leave the directory and those variables are removed from our shell.
1.1 Installation
1.1.1 Generic (rootless, works everywhere)
Download a release binary from GitHub and place it in our $PATH
:
This installs into ~/.local/bin
by default (create that directory if needed).
Make sure ~/.local/bin
is in our $PATH
:
Bash / Zsh
Fish
1.1.2 Package managers
1.1.3 Verify installation
After installation, hook direnv
into our shell so it runs on every prompt:
We’ll restart the shell afterwards.
Let’s check that direnv
is installed and hooked correctly:
If direnv status
shows errors or nothing about hooks, double-check that we’ve restarted our shell and that the hook line is present in our ~/.bashrc
, ~/.zshrc
, or fish config.
1.2 Using .envrc
A minimal .envrc
looks like this
- If we don’t define
RESULTS_DIR
in advance, it defaults to/tmp/results
. - If we export it manually (
export RESULTS_DIR=/scratch/me
), it takes precedence.
We activate it with:
This whitelists the .envrc
. If I later edit it (or pull changes from git), I must re-run direnv allow
. I can force a reload at any time with direnv reload
.
1.3 Benefits
- Variables are set before we run commands in that directory.
- Projects can have distinct
.envrc
files without clashing. - Defaults can be layered: project-specific defaults, common fallbacks, and user overrides.
1.4 Pitfalls
- Environment is tied to the current directory. If I
cd
somewhere else, the variables are automatically unloaded. Scripts I start keep the environment they inherit, but our interactive shell won’t. - If I open an interactive subshell with the
direnv
hook active and thencd
, the variables may be cleared inside that subshell too. Non-interactive shells are fine. - Because
.envrc
is executable Bash, it can run arbitrary code. We should review and allow it explicitly.
In practice, direnv
gives us the simplicity of .env
files while integrating with the shell, so configuration is language-agnostic and convenient for both Python scripts and general tools.
2 Python dotenv
One system I’ve used is dotenv. dotenv
allows easy configuration through OS environment variables or text files in the parent directory.
There are lots of packages with similar names but dissimilar functions.
Also similar are henriquebastos/python-decouple, sloria/environs. Dynaconf is sophisticated and comes closer to a full configuration system like hydra, and, as such, is too much for me.
Let’s imagine we’re using basic dotenv
for now, for concreteness. Then we can be indifferent to whether files come from an FS config or an environment variable.
import os, os.path
from dotenv import load_dotenv
load_dotenv() # take environment variables from .env.
# Code of your application, which uses environment variables (e.g. from `os.environ` or
# `os.getenv`) as if they came from the actual environment.
# substituting a var into a path:
DATA_FILE_PATH = os.path.expandvars('$DATA_PATH/$DATA_FILE')
# getting a var with a default fallback
FAVOURITE_PIZZA_TOPPING = os.getenv('FAVOURITE_PIZZA_TOPPING', 'cheese')
The datafile .env
is just a text file with lines like:
We also provide a CLI; it lets us run arbitrary commands with the correct environment variables set.
This only works for running Python scripts, as far as I can tell. [TODO clarify] So, why don’t we just use direnv?