DEV Community

Cover image for Python How-To: Creating And Using Environment Variables Part Two: Using .env Files
dev_neil_a
dev_neil_a

Posted on

Python How-To: Creating And Using Environment Variables Part Two: Using .env Files

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-*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

Save the file and then run it in a terminal with Python:

python3 example-one.py
Enter fullscreen mode Exit fullscreen mode

Output:

Hello world!
Enter fullscreen mode Exit fullscreen mode

Let us go over what happened in the example:

  1. 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 the dotenv library, specifically the load_dotenv function, which will create environment variables from a .env file.
  2. This step, load_env(), will look for a file called .env in the same location as where the code.py is stored. This can be overridden to use a different file name, which will be covered in the next example.
  3. 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!"
Enter fullscreen mode Exit fullscreen mode

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")}')
Enter fullscreen mode Exit fullscreen mode

Save the file and then run it in a terminal with Python:

python3 example-two.py
Enter fullscreen mode Exit fullscreen mode

Output:

Hello world, again!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or for Windows users, use:

set PRODUCT_STAGE=dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}""")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Finally, run the example-three.py script:

python3 example-three.py
Enter fullscreen mode Exit fullscreen mode

Output:

DB server name: localhost
DB server port: 5432
DB username: me
DB password: P@ssword123!
DB name: product_db
Enter fullscreen mode Exit fullscreen mode

That was a fair bit of code so let's go over what it did:

  1. Just as before, the os and dotenv libraries were imported. Additionally, the pathlib library was also imported so that a path to the .env-dev file could be built. The os library could be used for this but Path is easier to use.
  2. A variable called product_stage was defined that would take the value of the previously create PRODUCT_STAGE environment variable. In this case, it was set to dev.
  3. 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.
  4. 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.
  5. A final variable for the file path named full_path_to_file is defined. It will take the values of the env_files_folder_path and env_file_name variables and build the full path to the .env-dev file.
  6. In this stage, a check will be made to see if dotenv was able to load the .env-dev file. If it came back as True 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 of db_ variables that will be used in the next step. If the file was not found (dotenv will return False), a FileNotFoundError is raised and the program will stop.
  7. 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

Python dotenv documentation.

Python Keyring documentation.

Python os documentation.

Python pathlib documentation.

Top comments (0)