ADR 0028 — Use pydantic-settings for configuration management
Status
Accepted
Context
The microservices in this project (backend, chatbot, rag_service) need to manage configuration from environment variables and .env files. Previously, configuration was handled inconsistently across services:
- backend: Used a plain Python class with
os.getenv()andpython-dotenv - chatbot: Mixed
pydantic_settings.BaseSettingswithos.getenv()calls (incorrect usage) - rag_service: Similar mixed pattern with manual type conversions like
int(os.getenv(...))
This approach had several problems:
- Security risks: Sensitive values (API keys, passwords, secret keys) could be accidentally logged or exposed in error messages
- Inconsistent patterns: Each service handled configuration differently
- Type safety: Manual
int(),float()conversions were error-prone - Hardcoded defaults:
secret_key = "supersecretkey"was a security vulnerability - Code duplication: MongoDB URI construction logic was duplicated across services
Decision
Adopt pydantic-settings as the standard configuration management library across all Python microservices with the following conventions:
1. Use BaseSettings with SettingsConfigDict
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
2. Use SecretStr for sensitive values
secret_key: SecretStr = SecretStr("dev-only-secret-key-change-in-production")
mongo_root_password: SecretStr | None = None
gemini_api_key: SecretStr | None = None
This ensures secrets are displayed as ********** in logs and repr.
3. Use lowercase field names
Pydantic Settings automatically maps SECRET_KEY env var to secret_key field when case_sensitive=False.
4. Provide helper methods for complex configurations
def get_mongo_uri(self) -> str:
"""Construct MongoDB URI from configuration."""
if self.mongo_uri:
return self.mongo_uri.get_secret_value()
# ... construction logic
5. Remove python-dotenv dependency
pydantic-settings handles .env file loading natively; python-dotenv is no longer needed in service code.
Consequences
Positive
- Type safety: Automatic type coercion and validation
- Security:
SecretStrprevents accidental exposure of secrets in logs - Consistency: Same pattern across all services
- Less code: Removed ~50 lines of boilerplate (manual
os.getenv()calls, type conversions) - Better defaults: Descriptive default for
secret_keymakes it obvious it must be changed - Centralized logic: MongoDB URI construction in one place per service
Negative
- Migration effort: Required updating all settings usages to lowercase names
- Learning curve: Team members need to understand
SecretStr.get_secret_value()pattern - Slight verbosity: Accessing secrets requires
.get_secret_value()call
Follow-ups
- Update
backend/config.pyto usepydantic-settings - Update
chatbot/config.pyto removeos.getenv()mixing - Update
rag_service/config.pyto removeos.getenv()mixing - Update all usages of settings from
UPPERCASEtolowercase - Add
SecretStrfor all sensitive fields - Remove
python-dotenvfrom service dependencies - Verify all 102 tests pass
Alternatives considered
Option A: Continue with python-dotenv + os.getenv()
Pros:
- No migration needed
- Simple and familiar
Cons:
- No type safety
- No secret protection
- Inconsistent patterns
- More boilerplate code
Option B: Use dynaconf
Pros:
- Multi-format support (YAML, TOML, JSON)
- Environment layering
Cons:
- Additional dependency
- More complex than needed
- Less Pydantic integration
Option C: Use pydantic-settings (chosen)
Pros:
- Native Pydantic integration
- Type safety and validation
SecretStrfor security- Already partially adopted
Cons:
- Requires migration
- Team learning curve