Python How-To: Creating And Using Environment Variables Part Two: Using .env Files
Tags: python, tutorial, beginners, devops
Published: 2023-06-06 04:00:00



- [Introduction](#introduction)
- [What Are .env Files?](#what-are-env-files)
- [Creating And Using .env Files](#creating-and-using-env-files)
  - [Setup A .gitignore File](#setup-a-gitignore-file)
  - [Install dotenv](#install-dotenv)
  - [Example One: Using A .env File](#example-one-using-a-env-file)
  - [Example Two: Using A Different File Name](#example-two-using-a-different-file-name)
  - [Example Three: Multiple .env File Options](#example-three-multiple-env-file-options)
- [Conclusion](#conclusion)
- [References](#references)

## Introduction

This article is a continuation from [part one](https://dev.to/dev_neil_a/python-how-to-creating-and-using-environment-variables-part-one-1pac) 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:

``` console
.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](https://pypi.org/project/python-dotenv/) library. It provides an easy ay to load the contents of a .env file. To install it, use `pip` in a terminal:

``` console
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:

``` console
MY_VAR="Hello world!"
```

Save the file and close it.

Next, create a file called example-one.py and add the following code:

``` python
# --- 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:

``` console
python3 example-one.py
```

Output:

``` console
Hello world!
```

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:

``` console
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:

``` python
# --- 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:

``` console
python3 example-two.py
```

Output:

``` console
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:

``` console
export PRODUCT_STAGE=dev
```

Or for Windows users, use:

``` console
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:

``` console
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:

``` python
# --- 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:

``` console
- 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:

``` console
python3 example-three.py
```

Output:

``` console
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:

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](https://pypi.org/project/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.](https://pypi.org/project/python-dotenv/)

[Python Keyring documentation.](https://pypi.org/project/keyring/)

[Python os documentation.](https://docs.python.org/3/library/os.html)

[Python pathlib documentation.](https://docs.python.org/3/library/pathlib.html)