Python How-To: Creating And Using Environment Variables Part Two: Using .env Files
dev_neil_a
Posted on June 6, 2023
Introduction
This article is a continuation from part one that covered creating and using environment variables within Python by either creating them at the operating system level or within Python.
In this part, we will be looking at another method of creating and using environment variables by using .env files.
What Are .env Files?
So, what are .env files?
In Python, and other programming languages, .env files (or another name depending upon the language) are used to create a list of environment variables that can be loaded at runtime. Some typical examples include:
- API keys
- Database settings such as the:
- Server name
- Port number
- Username
- Password
- File paths and more
When the program finishes, be that it runs through normally, crashes or is cancelled by the user, the environment variables are cleared from the system. This means that they are no longer in memory to be accessed by the user or other users / programs on that system.
There are some security concerns with using .env files, namely that the information is stored in plain text on a file system. Also, in the case of the files being stored in a git tracked folder and repository, there is a risk that the .env file could be pushed to a repository hosting service where other users could access the information in that file.
Now, let's create some .env files and see some examples of how to use them.
Creating And Using .env Files
Setup A .gitignore File
The first thing to do before creating a .env file is to make sure that a .gitignore file is configured to ignore any .env files. This is only applicable for a git or other source code versioning / tracked project.
The .gitignore file is use to tell git to not track a file or folder that matches anything that is listed in the file. As .env files will contain information that is deemed as a security risk, they should not be pushed to a repository service, such as GitHub where anyone can access it (if it's a public repository).
Note: For the purposes of this article, it assumes that you have a git repository created.
In the folder which is being used, if a .gitignore file is not present, create a new file and call it .gitignore (no file extension). Next, open the file in a text editor such as VS Code and add the following to the end of the file:
.env
.env-*
Save the file and then commit & push the changes with git or however you choose to (VS Code Git plugin for example).
Note: The .env-*
entry will ignore any files that start .env-
and have anything after the -
.
Install dotenv
One of the simplest ways to work with .env files is to use the dotenv library. It provides an easy ay to load the contents of a .env file. To install it, use pip
in a terminal:
pip install python-dotenv
Now, let's have a look at some examples.
Example One: Using A .env File
First, let's create a .env file that will contain a single environment variable called MY_VAR
.
Create a file called .env in the same directory that the Python code will be placed in and add the following to it:
MY_VAR="Hello world!"
Save the file and close it.
Next, create a file called example-one.py and add the following code:
# --- 1. Import the required libraries and modules:
import os
from dotenv.main import load_dotenv
# --- 2. Load the contents of the .env file as environment variables:
load_dotenv()
# --- 3. Print out the value of MY_VAR:
print(os.getenv("MY_VAR"))
Save the file and then run it in a terminal with Python:
python3 example-one.py
Output:
Hello world!
Let us go over what happened in the example:
- The libraries that are required for this to work are imported. They are the
os
library, which is used to interact with the O/S, which in this case is to get environment variables. Next is thedotenv
library, specifically the load_dotenv function, which will create environment variables from a .env file. - This step,
load_env()
, will look for a file called.env
in the same location as where thecode.py
is stored. This can be overridden to use a different file name, which will be covered in the next example. - The last step is to print out the
MY_VAR
variable to the console, which in this example is 'Hello world!'.
Example Two: Using A Different File Name
This example will be very much the same as example one but instead of using a file called .env
, it will use a file called .env-example-two
.
First, create a new file called .env-example-two
in the same directory that the Python code will be placed in and add the following to it:
WORD_1="Hello"
WORD_2="World,"
WORD_3="Again!"
Save the file and close it.
Next, create a file called example-two.py and add the following code:
# --- 1. Import the required libraries and modules:
import os
from dotenv.main import load_dotenv
# --- 2. Load the contents of the .env-example-two file as environment variables:
load_dotenv(dotenv_path=".env-example-two")
# --- 3. Print out the value of the three environment variables:
print(f'{os.getenv("WORD_1")} {os.getenv("WORD_2")} {os.getenv("WORD_3")}')
Save the file and then run it in a terminal with Python:
python3 example-two.py
Output:
Hello world, again!
The differences with this example over the first one are that the file name has been specified in load_dotenv
to use the .env-example-two
file instead of the default .env file. The last difference is the number of environment variables being three rather than one.
Now, let's take a look at one more example that reflects a more real world use case.
Example Three: Multiple .env File Options
For our final example, there will be two sets of .env files, each with the same environment variable names but with different values. This is a common way to use .env files (where used) to provide, for example, database credentials and settings for a particular staging environment, such as development or testing.
First, in the terminal, create the following environment variable:
export PRODUCT_STAGE=dev
Or for Windows users, use:
set PRODUCT_STAGE=dev
This will be used in the program to determine the name of the file to use that contains the right variables for that stage, which in this case will be dev
.
Next, create a folder in the same directory and call it 'env'.
In that folder, create a new file and name it .env-dev
. Open that file in a text editor and add the following:
DB_SERVER_NAME=localhost
DB_SERVER_PORT=5432
DB_USERNAME=me
DB_PASSWORD=P@ssword123!
DB_NAME=product_db
Save the file and close it.
Next, make a copy of that file in the same folder and call it .env-test
. Open the file and change DB_SERVER_NAME=localhost
to DB_SERVER_NAME=db-server-01
and save the file.
Next, create a new file in the parent directory and call it example-three.py
. Open the file and add the following code to it:
# --- 1. Import the required libraries and modules:
import os
from dotenv.main import load_dotenv
from pathlib import Path
# --- 2. Get the name of the stage of the product from the O/S:
product_stage = os.getenv("PRODUCT_STAGE")
# --- 3. Define the path for the location of the .env files
base_dir = Path(__file__).resolve().parent # Get the folder path of where the Python file is.
env_files_folder_name = "env"
env_files_folder_path = Path(base_dir, env_files_folder_name)
# --- 4. Define the file name using the product_stage variable:
env_file_name = f".env-{product_stage}"
# --- 5. Define the full path to the .env-{product_stage} file:
full_path_to_file = Path(base_dir, env_files_folder_name, env_file_name)
# --- 6. Attempt to load the .env-{product_stage}.
if load_dotenv(dotenv_path=full_path_to_file) == True:
# --- If the file is present, define the below variables.
# --- If one is not present, it will have a value of "None":
db_server_name = os.getenv("DB_SERVER_NAME")
db_server_port = os.getenv("DB_SERVER_PORT")
db_username = os.getenv("DB_USERNAME")
db_password = os.getenv("DB_PASSWORD")
db_name = os.getenv("DB_NAME")
else:
# --- If the file cannot be found, raise a FileNotFound error and stop:
raise FileNotFoundError(
f"The file '{env_file_name}' does not exist in {env_files_folder_path}."
)
# --- 7. Display the contents of each variable:
print(f"""DB server name: {db_server_name}
DB server port: {db_server_port}
DB username: {db_username}
DB password: {db_password}
DB name: {db_name}""")
Save the file and then run it in a terminal with Python:
The final directory structure will look like this:
- env
| |
| |-- .env-dev
| |-- .env-test
|
|-- .env
|-- .env-example-two
|-- example-one.py
|-- example-two.py
|-- example-three.py
Finally, run the example-three.py script:
python3 example-three.py
Output:
DB server name: localhost
DB server port: 5432
DB username: me
DB password: P@ssword123!
DB name: product_db
That was a fair bit of code so let's go over what it did:
- Just as before, the
os
anddotenv
libraries were imported. Additionally, thepathlib
library was also imported so that a path to the.env-dev
file could be built. Theos
library could be used for this butPath
is easier to use. - A variable called
product_stage
was defined that would take the value of the previously createPRODUCT_STAGE
environment variable. In this case, it was set todev
. - A number of variables are defined that would result in a full path for where the
.env-dev
and.env-test
files are stored. This can be reduced to one variable but multiple were used to show what was happening in an easier to show manner. - A variable named
env_file_name
was defined that will have the name of which.env-
file to use. In this case, it will have the value.env-dev
. - A final variable for the file path named
full_path_to_file
is defined. It will take the values of theenv_files_folder_path
andenv_file_name
variables and build the full path to the.env-dev
file. - In this stage, a check will be made to see if
dotenv
was able to load the.env-dev
file. If it came back asTrue
then it was able to find the file and will then create the environment variables defined in the.env-dev
file. After that, it would then create a number ofdb_
variables that will be used in the next step. If the file was not found (dotenv will returnFalse
), aFileNotFoundError
is raised and the program will stop. - The final step will print out all the values of the
db_
variables.
In the real world, these db_
variables would be used to create a connection string to a database server and which database to use.
For a further example, change the value of the PRODUCT_STAGE
environment variable that was created in the terminal to testing
to see what happens.
Conclusion
This concludes this short series about environment variables but there are other ways to manage them. For example, there is a library called Keyring that can use a secure password store, such as Apple's KeyChain to create and retrieve credentials that can then be used just like in the examples shown.
Other secure password manager providers, such as 1Password and LastPass have Python libraries available that can be used to store and retrieve secure credentials. I would recommend one of those over using .env files.
Thank you for reading and have a nice day!
References
Posted on June 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.