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 equivalentsource
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.