BillJellesmaCoding

AboutBotPalsQuick Tip Tweets

Using Dotenv Files in Python

by Bill Jellesma
2021-06-24 23:50:00
Using Dotenv Files in Python

TL;DR

  • Add .env to .gitignore right away.
  • .env is an easily configurable file to store any values that may change in different environments.
  • Even if you build your code to an exe using pyinstaller, you can still specify the .env outside of your project.
  • A .env file should be in a secure location.
  • The concept of using .env files is not limited to Python and other languages have their own implementations.

What is an environmental variable

Environmental variables are any variables used in your code that you may want to change in different environments (staging vs production) for your project. This can be something like defining a different database host in local development versus production or it can be that you want a boolean variable called DEBUG that will log extra data when set to true. Environmental variables are built into hosts like Heroku, Netlify, and Amazon Web Services but you can also define them in your local development, typically using a file called .env.

Why define an environmental variable?

First off, an environmental variable is a variable so you're able to use it to repeat a value throughout your project, even throughout different files. It'd be pretty inconvenient to have to specify a new database connection everywhere the connection is used only to have to change that value later. Not only is this more typing, you'll also need to change the value of connection everywhere it's specified when you move databases.

model1.py

print('This is the first model`)
connection = 'http://exampledatabase.com:5432'

do_stuff(connection)

model2.py

print('This is the second model`)
connection = 'http://exampledatabase.com:5432'

do_more_stuff(connection)

Ideally, you'd want to use a third file that would be a central location where you can keep this connection string. The other files would then just import the central file.

central.py

CONNECTION = 'http://exampledatabase.com:5432'

model1.py

from central import CONNECTION

do_stuff(CONNECTION)

model2.py

from central import CONNECTION

do_more_stuff(CONNECTION)

We can also make this a little more modular and specify a hostname and port and then concatenate those together.

central.py

HOSTNAME = 'exampledatabase.com'
PORT = 5432

CONNECTION = f'http://{HOSTNAME}:{PORT}'

That central.py file helps us so that we only need to specify the variables in one file if we change the database in the future. A real world case for this would be if you connect to one database in development but then want to change that connection in staging or production.

This still leaves a problem that we have to modify our central.py. This is not ideal because we need to change python code, which could be more complex and "fragile". This is even more of an issue if we've compiled our code using something like pyinstaller because we need to rebuild our code every time we change the database. It could even be impossible if we don't have access to the source code to do a rebuild. This leads to the second advantage of environmental variables: Specifying values outside of the build process.

All we're doing is specifying values so do we need a python file? No, we just need something that tells our project the proper values to use. We can instruct our central.py file to read a JSON or YAML file, both of which are well supported for specifying settings like logging levels or debug mode. Commonly, a .env file is used for more sensitive content but you can also store settings.

Reading a dotenv file

The structure of a .env is very simple. It's just a text file assigning values to certain keys. You don't even need to specify quote marks around string values.

.env

HOSTNAME=exampledatabase.com
PORT=5432

As far as how to read this in python, we will use the help of a third party library python-dotenv. Simply install this library with pip install python-dotenv. The following code will read the .env file and print out the connection that we expect:

# import modules and load the env file
import os
from dotenv import load_dotenv
load_dotenv()

# Get the env vars
HOSTNAME = os.getenv('HOSTNAME')
PORT = os.getenv('PORT')

# Create the connection variable like before
CONNECTION = f'http://{HOSTNAME}:{PORT}'
print(f'connection: {CONNECTION}')

As you can see in the above, we need to import the load_dotenv() function as well as the os module. We can then create our CONNECTION variable the same as before. Now, our two models don't need to be changed at all since they're still importing the CONNECTION variable.

model1.py

from central import CONNECTION

do_stuff(CONNECTION)

model2.py

from central import CONNECTION

do_more_stuff(CONNECTION)

Now, even if you build the project using pyinstaller, you'll still be able to define a .env file in the root of your project that can be edited outside of the project. What's more is that this is a simple text file, it can easily be changed without needing to know how to write code. This makes it simple for a future developer, yourself, or even someone without a technical background to edit the file. Of course, this can be a double edged sword because everyone can edit this file. Therefore, security should be taken into consideration and not everyone should have access to this file.

You can actually specify the path to a .env file so that you don't have to use the root location. This can be useful if you want the program to get the variables from a secure location. To do this, we just need the pathlib module from the standard library.

# import modules and load the env file
import os
from dotenv import load_dotenv

# Specify a path to the .env file
from pathlib import Path
load_dotenv(dotenv_path=Path('./secret')/'.env')

# Get the env vars
HOSTNAME = os.getenv('HOSTNAME')
PORT = os.getenv('PORT')

# Create the connection variable like before
CONNECTION = f'http://{HOSTNAME}:{PORT}'
print(f'connection: {CONNECTION}')

Important Note

Since people will usually use a .env to store database information and other sensitive content, it's very important that this file doesn't get included in source control. If you're using Git as your source control of choice, this can be accomplished by adding .env to .gitignore.

.gitignore

.env

The above pattern will ignore any file called .env so even if you move your .env file, it should still be ignored by Git.

Wrap up

Although we've explored using a .env file and reading it with python, the concept of environmental variables are available in most languages.