Pydantic Settings¶
Type-safe configuration with environment variable support using Pydantic Settings.
Why Pydantic Settings?¶
- Type Safety — Configuration values are validated at startup
- Environment Variables — Automatic loading from environment
- Defaults — Sensible defaults with override capability
- Documentation — Self-documenting configuration
Basic Pattern¶
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class JWTServiceSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="JWT_")
secret_key: SecretStr
algorithm: str = "HS256"
access_token_expire_minutes: int = 15
This class:
- Reads from
JWT_SECRET_KEY,JWT_ALGORITHM,JWT_ACCESS_TOKEN_EXPIRE_MINUTES - Validates types automatically
- Provides defaults for
algorithmandaccess_token_expire_minutes - Keeps
secret_keysecure withSecretStr
Environment Variable Prefixes¶
Each settings class has its own prefix:
| Class | Prefix | Example Variables |
|---|---|---|
JWTServiceSettings |
JWT_ |
JWT_SECRET_KEY |
SecuritySettings |
DJANGO_ |
DJANGO_SECRET_KEY, DJANGO_DEBUG |
TelegramBotSettings |
TELEGRAM_BOT_ |
TELEGRAM_BOT_TOKEN |
AWSS3Settings |
AWS_S3_ |
AWS_S3_ACCESS_KEY_ID |
LogfireSettings |
LOGFIRE_ |
LOGFIRE_ENABLED, LOGFIRE_TOKEN |
LoggingConfig |
LOGGING_ |
LOGGING_LEVEL |
Computed Fields¶
Use @computed_field for derived values:
from datetime import timedelta
from pydantic import computed_field
class JWTServiceSettings(BaseSettings):
access_token_expire_minutes: int = 15
@computed_field()
def access_token_expire(self) -> timedelta:
return timedelta(minutes=self.access_token_expire_minutes)
Nested Settings¶
Compose settings with Field(default_factory=...):
from pydantic import Field
class CelerySettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="CELERY_")
redis_settings: RedisSettings = Field(default_factory=RedisSettings)
Secret Values¶
Use SecretStr for sensitive data:
from pydantic import SecretStr
class SecuritySettings(BaseSettings):
secret_key: SecretStr
# Usage
settings = SecuritySettings()
key = settings.secret_key.get_secret_value() # Explicit unwrapping required
SecretStr prevents accidental logging of secrets.
Complex Types¶
Lists¶
class HTTPSettings(BaseSettings):
allowed_hosts: list[str] = Field(default_factory=lambda: ["localhost"])
In .env:
Enums¶
from enum import StrEnum
class Environment(StrEnum):
LOCAL = "local"
STAGING = "staging"
PRODUCTION = "production"
class ApplicationSettings(BaseSettings):
environment: Environment = Environment.PRODUCTION
IoC Container Integration¶
Settings are registered in the container with factory functions:
def _register_services(container: Container) -> None:
container.register(
JWTServiceSettings,
factory=lambda: JWTServiceSettings(),
)
container.register(
JWTService,
scope=Scope.singleton,
)
The factory ensures environment variables are read during container setup.
Django Settings Adapter¶
Pydantic settings are converted to Django format:
# core/configs/django.py
from infrastructure.django.settings.pydantic_adapter import PydanticSettingsAdapter
application_settings = ApplicationSettings()
security_settings = SecuritySettings()
adapter = PydanticSettingsAdapter()
adapter.adapt(
application_settings,
security_settings,
settings_locals=locals(),
)
The adapter:
- Iterates over Pydantic model fields
- Converts field names to UPPER_CASE
- Unwraps
SecretStrvalues - Adds to Django's settings namespace
See Django Settings Adapter for details.
Validation¶
Pydantic validates values at instantiation:
This catches configuration errors early, during application startup.
Custom Validators¶
from pydantic import field_validator
class DatabaseSettings(BaseSettings):
conn_max_age: int = 600
@field_validator("conn_max_age")
@classmethod
def validate_conn_max_age(cls, v: int) -> int:
if v < 0:
raise ValueError("conn_max_age must be non-negative")
return v
Testing¶
Override settings in tests:
import os
def test_with_custom_settings():
os.environ["JWT_SECRET_KEY"] = "test-secret"
settings = JWTServiceSettings()
assert settings.algorithm == "HS256"
Or use .env.test:
Real Examples¶
Database Settings¶
class DatabaseSettings(BaseSettings):
default_auto_field: str = "django.db.models.BigAutoField"
conn_max_age: int = 600
database_url: str = "sqlite:///db.sqlite3"
@computed_field()
def databases(self) -> dict[str, Any]:
return {
"default": dj_database_url.parse(
self.database_url,
conn_max_age=self.conn_max_age,
),
}
S3 Storage Settings¶
class AWSS3Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AWS_S3_")
endpoint_url: str
access_key_id: str
secret_access_key: SecretStr
protected_bucket_name: str = "protected"
public_bucket_name: str = "public"
Related Topics¶
- Environment Variables Reference — All configuration options
- Django Settings Adapter — How settings are adapted
- Production Configuration — Production settings