Advanced Features

This guide covers advanced envdot features for power users and complex use cases.

Load Options

Override Behavior

Control whether existing values are overwritten:

from envdot import DotEnv

env = DotEnv('.env')

# Default: override existing values
env.load(override=True)

# Keep existing values, only add new ones
env.load(override=False)

OS Environment Sync

Control synchronization with os.environ:

# Apply to os.environ (default)
env.load(apply_to_os=True)

# Don't modify os.environ
env.load(apply_to_os=False)

# Set individual values without os.environ sync
env.set('INTERNAL_KEY', 'value', apply_to_os=False)

Multiple Configuration Files

Load from multiple sources:

from envdot import DotEnv

env = DotEnv(auto_load=False)

# Load base configuration
env.load('.env')

# Overlay environment-specific settings
env.load('.env.production', override=True)

# Add local overrides
env.load('.env.local', override=True)

Priority order (last wins):

  1. .env - Base configuration

  2. .env.production - Environment-specific

  3. .env.local - Local developer overrides

Environment-Based Loading

Load different configurations based on environment:

import os
from envdot import DotEnv

def load_config():
    env = DotEnv(auto_load=False)

    # Always load base config
    env.load('.env')

    # Load environment-specific config
    environment = os.getenv('ENVIRONMENT', 'development')
    env_file = f'.env.{environment}'

    try:
        env.load(env_file, override=True)
    except FileNotFoundError:
        pass  # Environment-specific file is optional

    return env

config = load_config()

Custom Type Handlers

For complex types, process values after loading:

from envdot import DotEnv
import json

env = DotEnv('.env')

# Parse JSON arrays
# ALLOWED_HOSTS=["localhost", "127.0.0.1"]
hosts_str = env.get('ALLOWED_HOSTS', cast_type=str)
allowed_hosts = json.loads(hosts_str) if hosts_str else []

# Parse comma-separated lists
# CORS_ORIGINS=http://localhost:3000,http://localhost:8080
origins_str = env.get('CORS_ORIGINS', default='')
cors_origins = [o.strip() for o in origins_str.split(',') if o.strip()]

Configuration Classes

Create structured configuration classes:

from dataclasses import dataclass
from envdot import DotEnv

@dataclass
class DatabaseConfig:
    host: str
    port: int
    name: str
    user: str
    password: str

@dataclass
class AppConfig:
    debug: bool
    port: int
    workers: int
    database: DatabaseConfig

def load_config() -> AppConfig:
    env = DotEnv('.env')

    db_config = DatabaseConfig(
        host=env.get('DB_HOST', default='localhost'),
        port=env.get('DB_PORT', default=5432),
        name=env.get('DB_NAME', default='app'),
        user=env.get('DB_USER', default='postgres'),
        password=env.get('DB_PASSWORD', default=''),
    )

    return AppConfig(
        debug=env.get('DEBUG', default=False),
        port=env.get('PORT', default=8000),
        workers=env.get('WORKERS', default=4),
        database=db_config,
    )

config = load_config()
print(f"Connecting to {config.database.host}:{config.database.port}")

Validation

Validate required variables:

from envdot import DotEnv

def validate_config(env: DotEnv):
    required_keys = [
        'DATABASE_URL',
        'SECRET_KEY',
        'API_KEY',
    ]

    missing = [key for key in required_keys if key not in env]

    if missing:
        raise ValueError(f"Missing required environment variables: {missing}")

env = DotEnv('.env')
validate_config(env)

Value Validation

Validate value ranges and formats:

from envdot import DotEnv
import re

def validate_values(env: DotEnv):
    # Validate port range
    port = env.get('PORT', default=8000)
    if not (1 <= port <= 65535):
        raise ValueError(f"PORT must be between 1 and 65535, got {port}")

    # Validate URL format
    db_url = env.get('DATABASE_URL')
    if db_url and not db_url.startswith(('postgresql://', 'mysql://')):
        raise ValueError("DATABASE_URL must be a valid database URL")

    # Validate secret key length
    secret = env.get('SECRET_KEY', default='')
    if len(secret) < 32:
        raise ValueError("SECRET_KEY should be at least 32 characters")

env = DotEnv('.env')
validate_values(env)

Context Managers

Use envdot in temporary contexts:

import os
from contextlib import contextmanager
from envdot import DotEnv

@contextmanager
def temp_env(**kwargs):
    """Temporarily set environment variables."""
    env = DotEnv(auto_load=False)
    original = {}

    # Save original values and set new ones
    for key, value in kwargs.items():
        original[key] = os.environ.get(key)
        env.set(key, value)

    try:
        yield env
    finally:
        # Restore original values
        for key, value in original.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value

# Usage
with temp_env(DEBUG=True, PORT=9000):
    # These values are only set within this block
    print(os.getenv('DEBUG'))  # True
    print(os.getenv('PORT'))   # 9000

# Original values restored here

Lazy Loading

Defer loading until first access:

from envdot import DotEnv

class LazyConfig:
    _instance = None
    _loaded = False

    @classmethod
    def get_instance(cls) -> DotEnv:
        if cls._instance is None:
            cls._instance = DotEnv('.env', auto_load=False)

        if not cls._loaded:
            cls._instance.load()
            cls._loaded = True

        return cls._instance

# First access triggers loading
config = LazyConfig.get_instance()

Watching for Changes

Monitor configuration file changes (requires watchdog):

from envdot import DotEnv
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class ConfigReloader(FileSystemEventHandler):
    def __init__(self, env: DotEnv, filepath: str):
        self.env = env
        self.filepath = filepath

    def on_modified(self, event):
        if event.src_path.endswith(self.filepath):
            print(f"Reloading configuration from {self.filepath}")
            self.env.load(override=True)

# Setup
env = DotEnv('.env')
handler = ConfigReloader(env, '.env')
observer = Observer()
observer.schedule(handler, '.', recursive=False)
observer.start()

Thread Safety

For multi-threaded applications:

import threading
from envdot import DotEnv

class ThreadSafeConfig:
    _lock = threading.Lock()
    _instance = None

    @classmethod
    def get_instance(cls) -> DotEnv:
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = DotEnv('.env')
        return cls._instance

# Thread-safe singleton access
config = ThreadSafeConfig.get_instance()

Testing with envdot

Mock configuration in tests:

import pytest
from envdot import DotEnv

@pytest.fixture
def mock_env(tmp_path):
    """Create a temporary .env file for testing."""
    env_file = tmp_path / '.env'
    env_file.write_text('''
        DEBUG=true
        PORT=8080
        DATABASE_URL=sqlite:///test.db
    ''')

    env = DotEnv(str(env_file))
    yield env

    # Cleanup
    env.clear(clear_os=True)

def test_config_loading(mock_env):
    assert mock_env.get('DEBUG') is True
    assert mock_env.get('PORT') == 8080

Isolation between tests:

@pytest.fixture(autouse=True)
def isolate_env():
    """Isolate environment between tests."""
    import os
    original_env = os.environ.copy()

    yield

    # Restore original environment
    os.environ.clear()
    os.environ.update(original_env)