What is Python dotenv

Posted in Python by Dirk - last update: Feb 15, 2024

python-dotenv is a Python library that helps manage environment variables in your Python projects by reading variables from a .env file and loading them into the environment.

Environment variables are typically used to store configuration settings, API keys, database connection strings, and other sensitive information outside of your codebase.

How to use Python dotenv

As python-dotenv is not part of the Python core you need to install it first. You can install it with pip:

pip install python-dotenv

First, create a .env file in the root of your project. In this file add your environment variables in the format KEY=VALUE. For example:

API_KEY=your_api_key
DATABASE_URL=mysql://user:password@localhost/db_name
DEBUG=True

In your Python script or application, you can use python-dotenv to load these variables into the environment:

from dotenv import load_dotenv

# Load variables from .env file into environment
load_dotenv()

# Access environment variables
api_key = os.getenv("API_KEY")
database_url = os.getenv("DATABASE_URL")
debug_mode = os.getenv("DEBUG")

# Now you can use these variables in your code

What is a .env file

A .env file, short for “environment,” is a plain text configuration file that is commonly used in software development to store environment variables. Environment variables are key-value pairs that hold configuration settings for an application. The .env file is usually placed in the root directory of a project, and it helps in managing configuration settings, especially sensitive information such as API keys, database credentials, or other environment-specific variables.

As the .env file often contains sensitive information, so it’s crucial not to commit it to version control systems like Git. Instead, add it to your project’s .gitignore file to ensure it is excluded.

Main functions from the python-dotenv library

dotenv.load_dotenv()

Syntax: dotenv.load_dotenv(dotenv_path=None, verbose=False, override=False)

The dotenv.load_dotenv() function loads environment variables from a specified .env file or the default .env file in the current working directory.

from dotenv import load_dotenv

# Load environment variables from the default .env file
load_dotenv()

# or load from a specific file
load_dotenv(dotenv_path='/path/to/custom/.env')

You can conditionally load different .env files based on whether your application is running in a development or production environment. Here’s an example using the dotenv module and a hypothetical Flask application:

Assume we have the following project structure:

project/
├── app/
│   └── main.py
├── env/
│   ├── .env.dev
│   └── .env.prod
└── requirements.txt

with:

  • main.py: Your main application code.
  • env/: Directory containing environment-specific .env files.

with 2 different .env files:

  • a development version .env.dev:
API_KEY=dev_api_key
DATABASE_URL=mysql://dev_user:dev_password@localhost/dev_db
DEBUG=True
  • a production version .env.prod:
API_KEY=prod_api_key
DATABASE_URL=mysql://prod_user:prod_password@localhost/prod_db
DEBUG=False

In your main.py file, you can conditionally load the appropriate environment file based on whether the application is running in a development or production environment:

from flask import Flask
from dotenv import load_dotenv
import os

# Determine the environment (e.g., based on an environment variable or other condition)
environment = os.environ.get('APP_ENV', 'development')  # Default to development if not set

# Define the path to the corresponding .env file
dotenv_path = f'.env.{environment}'

# Load environment variables from the chosen .env file
load_dotenv(dotenv_path)

app = Flask(__name__)

# Access environment variables
api_key = os.getenv("API_KEY")
database_url = os.getenv("DATABASE_URL")
debug_mode = os.getenv("DEBUG")

@app.route('/')
def home():
    return f'API Key: {api_key}, Database URL: {database_url}, Debug Mode: {debug_mode}'

if __name__ == '__main__':
    app.run(debug=True)

In this example, the dotenv_path variable is dynamically set based on the environment (defaulting to development). The load_dotenv function is then used to load the environment variables from the corresponding .env file.

dotenv.find_dotenv()

dotenv.find_dotenv() searches for the specified .env file starting from the current working directory and moving up the directory tree.

Syntax: dotenv.find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False)

  • filename: default .env - but can be overwritten (example: config.env)
  • raise_error_if_not_found - default value is False. If set to True, the function will raise a FileNotFoundError if the specified .env file is not found. Default it would return None when the .env is not found
  • usecwd- boolean, default is False. If set to True it will only search the current working directory.

Example:

from dotenv import find_dotenv

# Example 1: Default behavior with the default filename '.env'
dotenv_path_default = find_dotenv()
print(f"Default .env file found at: {dotenv_path_default}")

# Example 2: Custom filename
dotenv_path_custom = find_dotenv(filename='custom_env_file.env')
print(f"Custom .env file found at: {dotenv_path_custom}")

# Example 3: Raise an error if the file is not found
try:
    dotenv_path_error = find_dotenv(raise_error_if_not_found=True)
    print(f".env file found at: {dotenv_path_error}")
except FileNotFoundError:
    print("Error: .env file not found.")

In this example, find_dotenv is used in three scenarios:

  • It looks for the default .env file in the current working directory and its parent directories.
  • It looks for a custom .env file named custom_env_file.env.
  • It tries to find the .env file, but if it’s not found, it raises a FileNotFoundError, allowing you to handle the error explicitly.

Adjusting the filename parameter allows you to search for different environment variable files, and the raise_error_if_not_found parameter provides control over whether to raise an error when the specified file is not found. With the usecwdparameter you determine if the search should be limited to the current working directory or not.

dotenv.load_dotenv_stream()

This function is similar to dotenv.load_dotenv() but it loads environment variables from a file-like object (e.g., a file stream) instead of a file path.

Syntax: dotenv.load_dotenv_stream(stream, verbose=False, override=False)

Example

from dotenv import load_dotenv_stream
from io import StringIO

# Create a file-like object (e.g., StringIO)
stream = StringIO("KEY=VALUE\nANOTHER_KEY=ANOTHER_VALUE\n")

# Load environment variables from the stream
load_dotenv_stream(stream)

dotenv.dotenv_values(dotenv_path=None, verbose=False, override=False)

With dotenv.dotenv_values() you can load all the environment variables in a dictionary, without modifying the environment itself.

Syntax: dotenv.dotenv_values(dotenv_path=None, verbose=False, override=False)

Example

from dotenv import dotenv_values

# Get a dictionary of environment variables from the default .env file
env_variables = dotenv_values()

# or specify a custom file
env_variables = dotenv_values(dotenv_path='/path/to/custom/.env')

print(env_variables)

dotenv.set_key()

Syntax: dotenv.set_key(dotenv_path, key, value)

With dotenv you can not only read environment variables, it also allows you to create new environment variables or update existing ones in a specified .env file using the dotenv.set_key() function. It adds or updates a key-value pair in a specified .env file. Example:

from dotenv import set_key

# Add or update a key in the .env file
set_key(dotenv_path='/path/to/custom/.env', key='NEW_KEY', value='NEW_VALUE')

When to use python-dotenv

  • Local Development: When you are developing locally, you can use a .env file to store configuration settings without hardcoding them into your source code. This makes it easy to share the codebase without exposing sensitive information.

  • Docker Containers: When you deploy your application using Docker containers, you can use python-dotenv to load environment variables into the container.

  • Continuous Integration (CI) and Deployment: When you deploy your application to servers or cloud platforms, you can use python-dotenv to manage environment variables in a secure and organized way.

Example usage

Here’s a simple example using Flask, a web framework for Python:

from flask import Flask
from dotenv import load_dotenv
import os

# Load environment variables
load_dotenv()

app = Flask(__name__)

# Access environment variables
api_key = os.getenv("API_KEY")
database_url = os.getenv("DATABASE_URL")
debug_mode = os.getenv("DEBUG")

@app.route('/')
def home():
   return f'API Key: {api_key}, Database URL: {database_url}, Debug Mode: {debug_mode}'

if __name__ == '__main__':
   app.run(debug=True)

In this example, Flask uses environment variables for configuration, and python-dotenv helps load these variables from the .env file during development or deployment.

python-dotenv versus python-decouple

python-dotenv and python-decouple are both Python libraries that help manage configuration settings and environment variables in a project, but they have some differences in their approach and features.

Configuration File Format:

  • python-dotenv: It uses a simple .env file format where each line contains a key-value pair.
  • python-decouple: It uses a more flexible configuration file format, supporting multiple file formats such as .ini, .json, and others.

API and Usage:

  • python-dotenv: It provides a straightforward API to load environment variables from a .env file into the environment. You access variables using os.getenv() or os.environ.
  • python-decouple: It offers a more advanced API with a Config class, allowing you to define default values, type casting, and handling of different configuration file formats. You can access variables using attributes or methods of the Config class.

Type Casting:

  • python-dotenv: It does not inherently support type casting. All values are treated as strings, and if you need a specific type, you must convert it manually.
  • python-decouple: It supports type casting, allowing you to specify the expected type for each configuration variable. For example, you can define a variable as an integer or a boolean.

Default Values:

  • python-dotenv: It does not have built-in support for default values. If a variable is not present in the .env file, the corresponding os.getenv() call will return None.
  • python-decouple: It allows you to set default values for configuration variables. If a variable is not found in the configuration file, the default value is used.

Format Support:

  • python-dotenv: Primarily focused on loading environment variables from a .env file.
  • python-decouple: More versatile, supporting various file formats and providing additional features like comments in configuration files.

If you prefer a simple and straightforward approach with a focus on loading variables from a .env file, python-dotenv may be a better fit. If you need more advanced features, such as type casting and support for different file formats, python-decouple might be a better choice.

Other articles