Skip to main content

How to Create and Use .env Files in Python

Managing sensitive information like API keys, database credentials, and secret tokens directly in your source code is a serious security risk. .env files offer a clean, secure, and widely adopted solution: store your configuration in a simple text file and load it into your Python application at runtime.

This guide walks you through the entire process, from creating a .env file to reading its values in Python using the python-dotenv library, with practical examples, best practices, and common mistakes to avoid.

What Is a .env File?

A .env file is a plain text file that holds key-value pairs representing environment variables. Each line follows the format KEY=VALUE:

DATABASE_URL=postgresql://user:password@localhost:5432/mydb
SECRET_KEY=supersecretkey123
DEBUG=True

These files are not meant to be committed to version control. Instead, they stay on each developer's machine (or deployment environment), keeping sensitive data out of the codebase entirely.

Why Use .env Files?

  • Security: API keys and passwords stay out of your source code and Git history.
  • Portability: different environments (development, staging, production) can use different .env files without changing code.
  • Simplicity: configuration is centralized in one readable file.

Prerequisites

Before you begin, make sure you have:

  • Python 3 installed (download here)
  • pip available (it comes bundled with modern Python installations)

Step 1: Install the python-dotenv Library

The python-dotenv package reads .env files and loads the variables into your application's environment. Install it with pip:

pip install python-dotenv
tip

If you're using a virtual environment (recommended), activate it before running the install command so the package is scoped to your project.

Step 2: Create the .env File

In the root directory of your project, create a file named .env (note the leading dot). Add your configuration as key-value pairs:

API_KEY=your_api_key_here
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
SECRET_KEY=supersecretvalue
DEBUG=True
Important Rules for .env Files
  • No spaces around the = sign (some parsers tolerate them, but it's safest to omit them).
  • Use quotes around values that contain spaces: APP_NAME="My Cool App".
  • Lines starting with # are treated as comments: # This is a comment.
  • Never commit this file to version control.

Step 3: Add .env to .gitignore

To prevent accidentally pushing secrets to a repository, add .env to your .gitignore file:

# Environment variables
.env

It's also a good practice to provide a template file (.env.example) that documents the required variables without containing real values:

API_KEY=
DATABASE_URL=
SECRET_KEY=
DEBUG=

This way, other developers know which variables they need to configure.

Step 4: Load and Access Variables in Python

Use load_dotenv() to load the .env file, then access individual values with os.getenv():

import os
from dotenv import load_dotenv

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

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

print(f"API Key: {api_key}")
print(f"Database URL: {database_url}")
print(f"Debug Mode: {debug}")

Output:

API Key: your_api_key_here
Database URL: postgresql://user:password@localhost:5432/mydb
Debug Mode: True

How It Works

  1. load_dotenv() reads the .env file and injects its key-value pairs into the process's environment variables.
  2. os.getenv("KEY") retrieves the value associated with KEY from the environment.
  3. If a variable is not found, os.getenv() returns None by default.

Providing Default Values

If a variable might not exist in the .env file, you can supply a fallback value as the second argument to os.getenv():

import os
from dotenv import load_dotenv

load_dotenv()

# Falls back to 5432 if PORT is not defined in .env
port = os.getenv("PORT", "5432")
print(f"Port: {port}")

Output (when PORT is not in .env):

Port: 5432

Using dotenv_values() as an Alternative

If you prefer to load .env values into a dictionary instead of injecting them into the environment, use dotenv_values():

from dotenv import dotenv_values

config = dotenv_values(".env")

print(config["API_KEY"])
print(config["DATABASE_URL"])

Output:

your_api_key_here
postgresql://user:password@localhost:5432/mydb
note

This approach is useful when you want to keep .env values isolated from the system environment and avoid potential naming conflicts with existing environment variables.

Loading a Custom .env File Path

By default, load_dotenv() looks for a .env file in the current working directory. You can point it to a different file:

from dotenv import load_dotenv

# Load from a custom path
load_dotenv(dotenv_path="/path/to/config/.env.production")

This is especially handy when you maintain separate files per environment, such as .env.development, .env.staging, and .env.production.

Common Mistakes and How to Avoid Them

Mistake 1: Forgetting to Call load_dotenv()

import os

# ❌ .env values are NOT loaded: os.getenv returns None
api_key = os.getenv("API_KEY")
print(api_key)

Output:

None

Fix: Always call load_dotenv() before accessing the variables:

import os
from dotenv import load_dotenv

load_dotenv() # ✅ Load .env first

api_key = os.getenv("API_KEY")
print(api_key)

Mistake 2: Adding Spaces Around the = Sign

API_KEY = your_api_key_here

Some parsers interpret the key as API_KEY (with a trailing space), causing os.getenv("API_KEY") to return None.

Fix: Remove spaces around =:

API_KEY=your_api_key_here

Mistake 3: Committing .env to Version Control

Pushing your .env file to a public (or even private) repository exposes your secrets. Always add it to .gitignore before your first commit.

danger

If you've already committed a .env file, removing it from the repository is not enough: the secrets remain in the Git history. You'll need to rotate all exposed credentials and consider tools like git filter-repo to purge the file from history.

Complete Working Example

Here's a full end-to-end example putting everything together:

APP_NAME=MyPythonApp
APP_VERSION=1.0.0
SECRET_KEY=abc123xyz
DEBUG=True
main.py
import os
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# Access variables with fallback defaults
app_name = os.getenv("APP_NAME", "DefaultApp")
app_version = os.getenv("APP_VERSION", "0.0.1")
secret_key = os.getenv("SECRET_KEY")
debug = os.getenv("DEBUG", "False")

# Display the configuration
print(f"Application: {app_name} v{app_version}")
print(f"Debug Mode: {debug}")

# Verify that secret key is loaded
if secret_key:
print("Secret key loaded successfully.")
else:
print("WARNING: Secret key is missing!")

Output:

Application: MyPythonApp v1.0.0
Debug Mode: True
Secret key loaded successfully.

Conclusion

Using .env files in Python is a best practice for managing configuration and sensitive data. Combined with the python-dotenv library, the workflow is straightforward:

  1. Create a .env file with your key-value pairs.
  2. Add .env to .gitignore to keep secrets out of version control.
  3. Install python-dotenv and call load_dotenv() in your code.
  4. Access values with os.getenv() or use dotenv_values() for a dictionary approach.

This pattern keeps your credentials secure, makes your application portable across environments.