Environment variables are a standard way to manage configuration settings that vary between environments (e.g., development, testing, production). Instead of hardcoding sensitive data like API keys or database credentials, you can store them in a .env
file, which is not included in version control systems like Git.
Example .env
file:
DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
SECRET_KEY=supersecretkey
DEBUG=True
Setting Up python-dotenv
Installation
To work with .env
files in Python, install the python-dotenv
library:
pip install python-dotenv
This library allows you to load variables from a .env
file into your environment.
Loading Environment Variables
Basic Usage
Create a .env
file in the root directory of your project:
API_KEY=12345
ENV=development
DEBUG=False
Load these variables in your Python script using load_dotenv
:
from dotenv import load_dotenv
import os
# Load environment variables from the .env file
load_dotenv()
# Access variables
api_key = os.getenv("API_KEY")
env = os.getenv("ENV")
print(f"API Key: {api_key}")
print(f"Environment: {env}")
Using typing
for Type Safety
Environment variables are typically strings, but you may need to work with other types like integers, booleans, or lists. To ensure type safety, you can use Python's typing
module in combination with helper functions. Because some variables may not always be set, you should use Optional
from typing
to handle these gracefully.
Helper Function for Typed Environment Variables
Create a utility module (env_utils.py
) to handle typed environment variables:
from typing import Any, Optional, Type, TypeVar, cast
import os
T = TypeVar("T")
def get_env_var(key: str, var_type: Type[T], default: Optional[T] = None) -> Optional[T]:
value = os.getenv(key, default)
if value is None:
return default
if var_type == bool:
if type(value) is bool:
return value
return cast(T, value.lower() in ["true", "1", "yes"])
elif var_type == int:
try:
return cast(T, int(value))
except ValueError:
raise ValueError(f"Environment variable '{key}' must be an integer.")
elif var_type == float:
try:
return cast(T, float(value))
except ValueError:
raise ValueError(f"Environment variable '{key}' must be a float.")
elif var_type == str:
return cast(T, value)
else:
raise TypeError(f"Unsupported type '{var_type}' for environment variable '{key}'.")
This function:
- Fetches an environment variable.
- Converts it to the desired type (
int
,bool
,float
, etc.). - Raises errors for missing or invalid values.
Example Usage
from dotenv import load_dotenv
from env_utils import get_env_var
from typing import Optional
# Load .env variables
load_dotenv()
# Typed environment variables
api_key: Optional[str] = get_env_var("API_KEY", str)
debug_mode: bool = get_env_var("DEBUG", bool, default=False)
max_connections: int = get_env_var("MAX_CONNECTIONS", int, default=10)
if api_key is not None:
print(f"API Key: {api_key}, Type: {type(api_key)}")
else:
print("API Key is not set")
print(f"Debug Mode: {debug_mode}, Type: {type(debug_mode)}")
print(f"Max Connections: {max_connections}, Type: {type(max_connections)}")
Advanced Scenarios
Using Default Values
For non-critical variables, you can specify default values directly:
log_level: str = get_env_var("LOG_LEVEL", str, default="INFO")
Using Typed Data Classes for Configuration
For larger projects, encapsulate configuration settings in a typed data class:
from pydantic import BaseModel
from env_utils import get_env_var
from dotenv import load_dotenv
# Load .env variables
load_dotenv()
class AppConfig(BaseModel):
api_key: str = get_env_var("API_KEY", str)
debug: bool = get_env_var("DEBUG", bool, default=False)
database_url: str = get_env_var("DATABASE_URL", str)
config = AppConfig()
print(config)
Security Best Practices
Never Commit .env
Files: Add .env
to your .gitignore
file to avoid exposing sensitive information in version control.
# .gitignore
.env
Validate Variables: Always validate critical variables (e.g., API keys) to ensure they’re set correctly.
Conclusion
Using .env
files simplifies environment variable management in Python projects. By combining this with Python’s typing
module, you can ensure type safety, reducing runtime errors and making your code more robust. As your projects grow, you can adopt advanced techniques like typed data classes or secret management tools to maintain scalability and security.