Skip to content

Commit 8ef50c6

Browse files
committed
doc: add lazy loading documentation
1 parent 6b688c2 commit 8ef50c6

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed

docs/index.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,3 +2932,298 @@ mutable_settings.__init__()
29322932
print(mutable_settings.foo)
29332933
#> foo
29342934
```
2935+
2936+
## Lazy Loading
2937+
2938+
Lazy loading defers field value resolution until fields are actually accessed, rather than eagerly fetching all values during settings initialization. This is particularly useful when working with cloud secret managers where each field access triggers an API call, avoiding unnecessary network requests for fields that may never be used.
2939+
2940+
### Overview
2941+
2942+
By default, pydantic-settings eagerly resolves all field values from all configured sources during initialization. For cloud secret managers like AWS Secrets Manager, Azure Key Vault, or Google Cloud Secret Manager, this means every field triggers an API call to fetch the secret value, even if the application never uses that field.
2943+
2944+
**When to use lazy loading:**
2945+
2946+
* You use cloud secret managers (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager)
2947+
* Your settings have many fields but your application only uses a subset of them
2948+
* You want to reduce initialization time and API call costs
2949+
* Network latency to secret managers is significant
2950+
2951+
**Trade-offs:**
2952+
2953+
* Fields are resolved when first accessed, not during initialization, so errors surface later
2954+
* There's a small overhead on first access to each field (caching minimizes subsequent accesses)
2955+
* `model_dump()` and other full-model operations will trigger resolution of all fields
2956+
2957+
### Basic Usage
2958+
2959+
You can enable lazy loading in two ways:
2960+
2961+
**1. Global configuration via SettingsConfigDict:**
2962+
2963+
```py
2964+
from pydantic_settings import BaseSettings, SettingsConfigDict
2965+
2966+
2967+
class Settings(BaseSettings):
2968+
model_config = SettingsConfigDict(lazy_load=True)
2969+
2970+
api_key: str
2971+
database_url: str
2972+
debug_mode: bool
2973+
```
2974+
2975+
**2. Per-source configuration via settings_customise_sources:**
2976+
2977+
```py
2978+
import os
2979+
2980+
from pydantic_settings import (
2981+
AWSSecretsManagerSettingsSource,
2982+
BaseSettings,
2983+
PydanticBaseSettingsSource,
2984+
)
2985+
2986+
2987+
class Settings(BaseSettings):
2988+
api_key: str
2989+
database_url: str
2990+
2991+
@classmethod
2992+
def settings_customise_sources(
2993+
cls,
2994+
settings_cls: type[BaseSettings],
2995+
init_settings: PydanticBaseSettingsSource,
2996+
env_settings: PydanticBaseSettingsSource,
2997+
dotenv_settings: PydanticBaseSettingsSource,
2998+
file_secret_settings: PydanticBaseSettingsSource,
2999+
) -> tuple[PydanticBaseSettingsSource, ...]:
3000+
aws_settings = AWSSecretsManagerSettingsSource(
3001+
settings_cls,
3002+
secret_id=os.environ['AWS_SECRET_ID'],
3003+
lazy_load=True, # Enable lazy loading only for AWS Secrets Manager
3004+
)
3005+
return (
3006+
init_settings,
3007+
env_settings,
3008+
dotenv_settings,
3009+
aws_settings,
3010+
file_secret_settings,
3011+
)
3012+
```
3013+
3014+
### Cloud Secret Managers with Lazy Loading
3015+
3016+
Lazy loading provides significant performance benefits when using cloud secret managers. Here are examples for each provider:
3017+
3018+
#### AWS Secrets Manager
3019+
3020+
```py
3021+
import os
3022+
from unittest.mock import MagicMock, patch
3023+
3024+
from pydantic import Field
3025+
3026+
from pydantic_settings import (
3027+
AWSSecretsManagerSettingsSource,
3028+
BaseSettings,
3029+
PydanticBaseSettingsSource,
3030+
)
3031+
3032+
3033+
class Settings(BaseSettings):
3034+
# These fields will only be fetched from AWS Secrets Manager when accessed
3035+
api_key: str = Field(validation_alias='MyApiKey')
3036+
database_password: str = Field(validation_alias='DbPassword')
3037+
encryption_key: str = Field(validation_alias='EncryptionKey')
3038+
3039+
@classmethod
3040+
def settings_customise_sources(
3041+
cls,
3042+
settings_cls: type[BaseSettings],
3043+
init_settings: PydanticBaseSettingsSource,
3044+
env_settings: PydanticBaseSettingsSource,
3045+
dotenv_settings: PydanticBaseSettingsSource,
3046+
file_secret_settings: PydanticBaseSettingsSource,
3047+
) -> tuple[PydanticBaseSettingsSource, ...]:
3048+
# Mock AWS credentials for demonstration
3049+
with patch.dict(os.environ, {'AWS_SECRETS_MANAGER_SECRET_ID': 'my-secret'}):
3050+
aws_secrets = AWSSecretsManagerSettingsSource(
3051+
settings_cls,
3052+
secret_id=os.environ['AWS_SECRETS_MANAGER_SECRET_ID'],
3053+
lazy_load=True,
3054+
)
3055+
return (
3056+
init_settings,
3057+
env_settings,
3058+
dotenv_settings,
3059+
aws_secrets,
3060+
file_secret_settings,
3061+
)
3062+
3063+
3064+
# Initialization is fast - no API calls made yet
3065+
settings = Settings()
3066+
3067+
# With lazy_load=True, accessing fields would trigger AWS API calls
3068+
# In production, this would fetch from AWS Secrets Manager
3069+
# settings.api_key # Would call AWS API if credentials were available
3070+
```
3071+
3072+
#### Azure Key Vault
3073+
3074+
```py
3075+
from unittest.mock import MagicMock, patch
3076+
3077+
from pydantic import Field
3078+
3079+
from pydantic_settings import (
3080+
AzureKeyVaultSettingsSource,
3081+
BaseSettings,
3082+
PydanticBaseSettingsSource,
3083+
)
3084+
3085+
3086+
class Settings(BaseSettings):
3087+
api_key: str = Field(validation_alias='MyApiKey')
3088+
database_password: str = Field(validation_alias='DbPassword')
3089+
3090+
@classmethod
3091+
def settings_customise_sources(
3092+
cls,
3093+
settings_cls: type[BaseSettings],
3094+
init_settings: PydanticBaseSettingsSource,
3095+
env_settings: PydanticBaseSettingsSource,
3096+
dotenv_settings: PydanticBaseSettingsSource,
3097+
file_secret_settings: PydanticBaseSettingsSource,
3098+
) -> tuple[PydanticBaseSettingsSource, ...]:
3099+
# Mock Azure credentials for demonstration
3100+
with patch('pydantic_settings.sources.providers.azure.import_azure_key_vault'):
3101+
with patch('pydantic_settings.sources.providers.azure.SecretClient'):
3102+
mock_credential = MagicMock()
3103+
azure_keyvault = AzureKeyVaultSettingsSource(
3104+
settings_cls,
3105+
vault_url='https://myvault.vault.azure.net/',
3106+
credential=mock_credential,
3107+
lazy_load=True,
3108+
)
3109+
return (
3110+
init_settings,
3111+
env_settings,
3112+
dotenv_settings,
3113+
azure_keyvault,
3114+
file_secret_settings,
3115+
)
3116+
3117+
3118+
# Quick initialization - no Azure API calls made
3119+
settings = Settings()
3120+
3121+
# Fields resolved on first access (would call Azure API in production)
3122+
# settings.api_key
3123+
```
3124+
3125+
#### Google Cloud Secret Manager
3126+
3127+
```py
3128+
import os
3129+
from unittest.mock import MagicMock, patch
3130+
3131+
from pydantic import Field
3132+
3133+
from pydantic_settings import (
3134+
BaseSettings,
3135+
GoogleSecretManagerSettingsSource,
3136+
PydanticBaseSettingsSource,
3137+
)
3138+
3139+
3140+
class Settings(BaseSettings):
3141+
api_key: str = Field(validation_alias='MyApiKey')
3142+
database_password: str = Field(validation_alias='DbPassword')
3143+
3144+
@classmethod
3145+
def settings_customise_sources(
3146+
cls,
3147+
settings_cls: type[BaseSettings],
3148+
init_settings: PydanticBaseSettingsSource,
3149+
env_settings: PydanticBaseSettingsSource,
3150+
dotenv_settings: PydanticBaseSettingsSource,
3151+
file_secret_settings: PydanticBaseSettingsSource,
3152+
) -> tuple[PydanticBaseSettingsSource, ...]:
3153+
# Mock GCP credentials for demonstration
3154+
with patch('pydantic_settings.sources.providers.gcp.google_auth_default', return_value=(MagicMock(), 'my-project')):
3155+
with patch('pydantic_settings.sources.providers.gcp.SecretManagerServiceClient'):
3156+
gcp_secrets = GoogleSecretManagerSettingsSource(
3157+
settings_cls,
3158+
project_id=os.environ.get('GCP_PROJECT_ID', 'my-project'),
3159+
lazy_load=True,
3160+
)
3161+
return (
3162+
init_settings,
3163+
env_settings,
3164+
dotenv_settings,
3165+
gcp_secrets,
3166+
file_secret_settings,
3167+
)
3168+
3169+
3170+
# Fast initialization - no GCP API calls made
3171+
settings = Settings()
3172+
3173+
# First access would trigger GCP Secret Manager API call (in production)
3174+
# settings.api_key
3175+
```
3176+
3177+
### Behavior and Caching
3178+
3179+
When lazy loading is enabled:
3180+
3181+
1. **Initialization**: Settings are created with minimal overhead. Sources return empty dictionaries instead of eagerly fetching all values.
3182+
3183+
2. **First Access**: When you access a field for the first time (e.g., `settings.api_key`), the value is fetched from the configured source and cached in memory.
3184+
3185+
3. **Subsequent Access**: Accessing the same field again returns the cached value without making another API call.
3186+
3187+
4. **All Fields**: Iteration over all fields (via `model_dump()`, etc.) will trigger resolution of all fields at once.
3188+
3189+
```py
3190+
from pydantic_settings import (
3191+
AWSSecretsManagerSettingsSource,
3192+
BaseSettings,
3193+
PydanticBaseSettingsSource,
3194+
)
3195+
3196+
3197+
class Settings(BaseSettings):
3198+
secret1: str
3199+
secret2: str
3200+
3201+
@classmethod
3202+
def settings_customise_sources(
3203+
cls,
3204+
settings_cls: type[BaseSettings],
3205+
init_settings: PydanticBaseSettingsSource,
3206+
env_settings: PydanticBaseSettingsSource,
3207+
dotenv_settings: PydanticBaseSettingsSource,
3208+
file_secret_settings: PydanticBaseSettingsSource,
3209+
) -> tuple[PydanticBaseSettingsSource, ...]:
3210+
aws_settings = AWSSecretsManagerSettingsSource(
3211+
settings_cls, secret_id='my-secret', lazy_load=True
3212+
)
3213+
return (init_settings, env_settings, dotenv_settings, aws_settings, file_secret_settings)
3214+
3215+
3216+
settings = Settings()
3217+
3218+
# Only secret1 is fetched from AWS Secrets Manager
3219+
print(settings.secret1)
3220+
3221+
# secret1 is already cached, so this doesn't trigger another API call
3222+
print(settings.secret1)
3223+
3224+
# secret2 is fetched on first access
3225+
print(settings.secret2)
3226+
3227+
# This triggers fetching of all remaining fields not yet accessed
3228+
all_values = settings.model_dump()
3229+
```

0 commit comments

Comments
 (0)