Last week, one of my Multiverse apprentices asked me if I knew how to load a .env file in the Django web framework for Python. A .env file is used to store environment variables for a project. These typically include settings such as database connection strings, API keys, or other configuration parameters. The purpose of a .env file is twofold: to separate configuration from code, and to keep sensitive information out of version control systems such as Git. A .env file is typically specified in a .gitignore file to keep it untracked.

Here’s an example .env file:

DATABASE_URL=postgres://username:password@localhost:5432/mydatabase
API_KEY=abcdef123456
DEBUG=true

A Python script

Being more familiar with JavaScript than Python, I had previously used the dotenv npm package to load environment variables from a .env file into process.env. I assumed my apprentice needed something similar for Python, so I found some packages such as python-decouple and python-dotenv. However, my apprentice told me that their mentor sent them an article on freeCodeCamp which made me think they were supposed to do it in vanilla Python. I came up with a simple script that assumes the .env file is in the current directory:

import os
from pathlib import Path

path = Path(__file__).parent / ".env"
contents = path.read_text()
lines = contents.splitlines()

for line in lines:
key, val = line.split("=")
os.environ[key] = val

The dot command

The script worked, but I was mistaken. My apprentice was supposed to load the environment variables into their current shell session before running the Django CLI commands. I knew that it was possible to set environment variables in a Unix shell (like Bash or Zsh) like so:

export API_KEY=abcdef123456

But frankly, I’m a noob when it comes to shell scripting, and I didn’t know about the dot command (.). It evaluates the commands in a file as if they were entered directly into the terminal for the current shell session. There is an equivalent source command in some Unix shells, but the dot command is the POSIX standard.

Let’s assume our current directory contains a .env file with the following contents:

API_KEY=abcdef123456

To evaluate the contents of the .env file in our current shell session, we can run the following command:

. ./.env

If we try to print the value of the API_KEY variable, we’ll see that it does exist:

echo $API_KEY # should print "abcdef123456"

Now let’s assume our current directory contains a temp.py file with the following contents:

import os

API_KEY = os.environ.get("API_KEY")
print(API_KEY)

Let’s try to run the file:

python3 ./temp.py

Instead of seeing "abcdef123456" printed out, we’ll see None! Why is this? The .env file doesn’t contain the export keyword, which is a problem. There’s a difference between “regular” variables and environment variables.

Parent and child processes

Parent and child processes are a fundamental principle of Unix-like operating systems. A parent process initiates the creation of another process, known as a child process. A child process inherits environment variables, but not “regular” variables.

When we run python3 ./temp.py, our current shell session is a parent process that spawns a child process. Because the .env file does not contain the export keyword, the API_KEY variable was interpreted as a “regular” variable, not an environment variable, and is therefore unavailable to the child process.

Automatically exporting variables

The only standard way to export variables is to use the export keyword. Because a .env file does not usually contain the export keyword, this isn’t really an option. However, common Unix shells such as Bash and Zsh provide the functionality to automatically export variables. We need to enable this functionality, evaluate the .env file, then disable the functionality again.

Bash

In Bash, we can run set -a to enable the allexport option, and set +a to disable it. It’s weird that -a is used to enable the option and +a is used to disable the option. I don’t know why that is. The semicolons are used to enter multiple commands on the same line.

set -a ; . ./.env ; set +a

Zsh

In Zsh, we can run setopt allexport to enable the allexport option, and unsetopt allexport to disable it. Again, the semicolons are used to enter multiple commands on the same line.

setopt allexport ; . ./.env ; unsetopt allexport

Running the Python script again

Assuming we are using Bash or Zsh, and we have run those commands, the temp.py script should print out the value of the API_KEY environment variable.

Summary

  • A .env file is used to store environment variables for a project. These typically include settings such as database connection strings, API keys, or other configuration parameters.
  • The purpose of a .env file is to separate configuration from code and to keep sensitive information out of version control systems.
  • The dot command (.) evaluates the commands in a file as if they were entered directly into the terminal for the current shell session. There is an equivalent source command in some Unix shells, but the dot command is standard.
  • “Regular” variables are only available to the current shell session, which is a parent process. Environment variables are also available to child processes.
  • In some Unix shells, variables can be automatically exported.
  • To load the environment variables from a .env file into the current shell session, we need to enable automatic exports, evaluate the .env file, then disable automatic exports once more.