Configuring my code with “.env” files

2017-04-18 — 2026-08-09

quality 6.9

Wherein the Author Abandons Python’s Dotenv for the Shell-Native Tool Direnv, and Details How Per-Directory .Envrc Files Silently Load and Unload Variables Upon Entering and Leaving Folders.

computers are awful
POSIX
python

Assumed audience:

People who run code on multiple machines

Figure 1

Tips for configuring ML and other apps.

There are many tools to load environment variables from local files. A good resource 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.

Previously this page was about the python tool dotenv. But actually, why bother restricting ourselves to python? Let’s configure environment variables from the shell where we actually need them. I have now switched to direnv, which does everything I need.

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 the project root and direnv will evaluate it whenever we cd into that directory. Leave the directory, and direnv removes those variables from our shell.

1.1 Installation

brew install direnv      # homebrew
sudo apt install direnv  # debian etc

After installation, hook direnv into the shell so it runs on every prompt:

echo 'eval "$(direnv hook bash)"' >> ~/.bashrc  # Bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc # Zsh
echo 'direnv hook fish | source' >> ~/.config/fish/config.fish # Fish

If installed and hooked up correctly:

direnv version     # should print the installed version
direnv status      # should show "Loaded RC allowed 0" if no .envrc is active

1.2 Using .envrc

A minimal .envrc looks like standard bash:

export DATA_PATH="$HOME/data"                        # set a var
export RESULTS_DIR="${RESULTS_DIR:-/tmp/results}"    # set a var if not set
  • 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 for a given dir with:

direnv allow

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.

There is a dotenv compat layer. Just put dotenv in the .envrc and it will load a .env file in the same directory.

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 in interactive shells. If I cd somewhere else, the variables automatically unload — which is kind of the point.
  • Because .envrc is executable Bash, it can run arbitrary code, so we need to review and explicitly allow it.

In practice, direnv has 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.

pip install python-dotenv # or
conda install -c conda-forge python-dotenv

Also similar are henriquebastos/python-decouple and 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')

,a,32,C

Where “b” is customer name and “32” is temperature, “C” is unit. We wrote a script to loop through the file, but it dont handle when temperature missing. So error’s thrown and script crash’s. Its annoying. We should of added error handling before but we didnt, my bad.

DATA_PATH=/home/username/data
DATA_FILE=foo.csv
FAVOURITE_PIZZA_TOPPING=anchovies

We also provide a CLI; it lets us run arbitrary commands with the correct environment variables set.

pip install "python-dotenv[cli]"
dotenv run my_cool_script.py

This only works for running Python scripts, as far as I can tell. So, why don’t we just use direnv?