Beginner
Python Environment Variables: Quick Example
Environment variables keep sensitive data like API keys and database passwords out of your code. Python’s os.environ reads them, and python-dotenv loads them from a .env file.
#quick_example.py
import os
from dotenv import load_dotenv # pip install python-dotenv
load_dotenv() # reads .env file into environment variables
api_key = os.environ.get('API_KEY', 'not-set') # get with a fallback
db_host = os.environ.get('DB_HOST', 'localhost')
print(f"API Key: {api_key[:8]}...") # only show first 8 chars
print(f"DB Host: {db_host}")
Output:
API Key: sk-abc12...
DB Host: db.example.com
The load_dotenv() function reads key-value pairs from a .env file and makes them available through os.environ. Your secrets stay out of your codebase.
Want more? Below we cover creating .env files, keeping secrets out of Git, and a real-life database connection manager.
Why Environment Variables Matter for Python Developers
Hardcoding secrets into your source code is one of the most common security mistakes developers make. Push your code to GitHub with an API key embedded and bots will find it within minutes — that’s not an exaggeration. Environment variables solve this by keeping configuration separate from code. Different environments (dev, staging, production) can use different values without changing a single line of Python.
Using os.environ to Read Environment Variables in Python
Python’s built-in os module gives you direct access to environment variables through os.environ, which behaves like a dictionary.
#os_environ.py
import os
# Read an environment variable (raises KeyError if missing)
# home = os.environ['HOME']
# Safer: use .get() with a default value
home = os.environ.get('HOME', '/tmp')
user = os.environ.get('USER', 'unknown')
path = os.environ.get('PATH', '')
print(f"Home: {home}")
print(f"User: {user}")
print(f"PATH entries: {len(path.split(':'))}")
# Check if a variable exists
if 'API_KEY' in os.environ:
print("API_KEY is set")
else:
print("API_KEY is NOT set — using defaults")
Output:
Home: /home/user
User: user
PATH entries: 8
API_KEY is NOT set — using defaults
Always use .get() with a default value instead of direct dictionary access. If the variable doesn’t exist, os.environ['KEY'] throws a KeyError that will crash your script.
Creating a .env File for Your Python Project
A .env file is a simple text file with key-value pairs. Create it in your project root:
# .env
# Database settings
DB_HOST=db.example.com
DB_PORT=5432
DB_NAME=myapp
DB_USER=admin
DB_PASSWORD=supersecretpassword123
# API keys
API_KEY=sk-abc123def456ghi789
STRIPE_SECRET=sk_test_abcdefgh
# App settings
DEBUG=True
LOG_LEVEL=INFO
Note: Lines starting with # are comments. No quotes needed around values unless they contain spaces. No spaces around the = sign.
Installing and Using python-dotenv
pip install python-dotenv
Once installed, load_dotenv() reads your .env file and loads each variable into os.environ:
#using_dotenv.py
import os
from dotenv import load_dotenv
# Load .env file from the current directory (or specify a path)
load_dotenv() # looks for .env in current dir and parent dirs
# Now all .env variables are available via os.environ
db_config = {
'host': os.environ.get('DB_HOST'),
'port': int(os.environ.get('DB_PORT', 5432)),
'name': os.environ.get('DB_NAME'),
'user': os.environ.get('DB_USER'),
'password': os.environ.get('DB_PASSWORD'),
}
print(f"Connecting to {db_config['name']}@{db_config['host']}:{db_config['port']}")
print(f"Debug mode: {os.environ.get('DEBUG')}")
Output:
Connecting to myapp@db.example.com:5432
Debug mode: True
By default, load_dotenv() won’t overwrite existing environment variables. If you need to override them (for testing), pass override=True.
Keeping Secrets Out of Git With .gitignore
The whole point of using .env files is to keep secrets out of version control. Add .env to your .gitignore immediately:
# .gitignore
.env
.env.local
.env.production
*.env
Create a .env.example file that shows the required variables without actual values. Commit this to Git so other developers know what to set up:
# .env.example — copy to .env and fill in your values
DB_HOST=
DB_PORT=5432
DB_NAME=
DB_USER=
DB_PASSWORD=
API_KEY=
DEBUG=False
Validating Environment Variables at Startup
Don’t wait until your app crashes halfway through to discover a missing variable. Validate everything at startup.
#validate_env.py
import os
import sys
from dotenv import load_dotenv
load_dotenv()
REQUIRED_VARS = ['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'API_KEY']
missing = [var for var in REQUIRED_VARS if not os.environ.get(var)]
if missing:
print(f"ERROR: Missing required environment variables: {', '.join(missing)}")
print("Copy .env.example to .env and fill in the values")
sys.exit(1)
print("All required environment variables are set")
Output (when variables are missing):
ERROR: Missing required environment variables: API_KEY
Copy .env.example to .env and fill in the values
Real-Life Example: A Database Connection Manager
Here’s a practical example that combines everything — loading config from .env, validating required variables, and creating a reusable database configuration class.
#db_manager.py
import os
import sys
from dotenv import load_dotenv
from dataclasses import dataclass
load_dotenv()
@dataclass
class DatabaseConfig:
host: str
port: int
name: str
user: str
password: str
ssl: bool = True
@classmethod
def from_env(cls):
"""Create config from environment variables"""
required = ['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD']
missing = [v for v in required if not os.environ.get(v)]
if missing:
print(f"Missing DB config: {', '.join(missing)}")
sys.exit(1)
return cls(
host=os.environ['DB_HOST'],
port=int(os.environ.get('DB_PORT', 5432)),
name=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
ssl=os.environ.get('DB_SSL', 'true').lower() == 'true'
)
@property
def connection_string(self):
ssl_param = '?sslmode=require' if self.ssl else ''
return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}{ssl_param}"
# Usage
config = DatabaseConfig.from_env()
print(f"Database: {config.name}")
print(f"Host: {config.host}:{config.port}")
print(f"SSL: {config.ssl}")
# In production, you'd pass config.connection_string to your ORM
print(f"Connection string ready (password hidden)")
Output:
Database: myapp
Host: db.example.com:5432
SSL: True
Connection string ready (password hidden)
This pattern gives you type-safe configuration, validation at startup, sensible defaults, and a clean connection string builder — all powered by a simple .env file.

Frequently Asked Questions
What is the difference between os.environ and os.getenv() in Python?
os.environ.get('KEY') and os.getenv('KEY') are functionally identical — both return None if the variable is missing. The only difference is os.environ['KEY'] (without .get) raises a KeyError, while os.getenv always returns the default.
Can I use .env files in production?
You can, but most production deployments set environment variables directly through the hosting platform (Heroku config vars, AWS Parameter Store, Docker environment). The .env file is primarily a development convenience.
Does python-dotenv work with Django and Flask?
Yes. Flask has built-in .env support with python-dotenv. For Django, call load_dotenv() at the top of your settings.py before referencing any os.environ calls.
Conclusion
Environment variables are the right way to manage configuration and secrets in Python. Use python-dotenv for local development, validate required variables at startup, never commit .env to Git, and provide a .env.example for your team. It takes five minutes to set up and saves you from a world of security headaches.
Reference
python-dotenv documentation: https://pypi.org/project/python-dotenv/
12-Factor App config: https://12factor.net/config
Related Articles
Further Reading: For more details, see the official Python os.environ documentation.