Skip to content

Commit e401bc4

Browse files
committed
doc: add lazy loading documentation
1 parent 2de7550 commit e401bc4

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed

docs/index.md

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,3 +2932,331 @@ 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 logging
3022+
import os
3023+
3024+
from pydantic import Field
3025+
3026+
from pydantic_settings import (
3027+
AWSSecretsManagerSettingsSource,
3028+
BaseSettings,
3029+
PydanticBaseSettingsSource,
3030+
)
3031+
3032+
logging.basicConfig(level=logging.INFO)
3033+
logger = logging.getLogger(__name__)
3034+
3035+
3036+
class Settings(BaseSettings):
3037+
# These fields will only be fetched from AWS Secrets Manager when accessed
3038+
api_key: str = Field(validation_alias='MyApiKey')
3039+
database_password: str = Field(validation_alias='DbPassword')
3040+
encryption_key: str = Field(validation_alias='EncryptionKey')
3041+
3042+
@classmethod
3043+
def settings_customise_sources(
3044+
cls,
3045+
settings_cls: type[BaseSettings],
3046+
init_settings: PydanticBaseSettingsSource,
3047+
env_settings: PydanticBaseSettingsSource,
3048+
dotenv_settings: PydanticBaseSettingsSource,
3049+
file_secret_settings: PydanticBaseSettingsSource,
3050+
) -> tuple[PydanticBaseSettingsSource, ...]:
3051+
aws_secrets = AWSSecretsManagerSettingsSource(
3052+
settings_cls,
3053+
secret_id=os.environ.get('AWS_SECRETS_MANAGER_SECRET_ID', 'my-secret-id'),
3054+
lazy_load=True,
3055+
)
3056+
return (
3057+
init_settings,
3058+
env_settings,
3059+
dotenv_settings,
3060+
aws_secrets,
3061+
file_secret_settings,
3062+
)
3063+
3064+
3065+
# Initialization is fast - no API calls made yet
3066+
try:
3067+
settings = Settings()
3068+
logger.info('Settings initialized instantly (no AWS calls yet)')
3069+
# With lazy_load=True, field access triggers AWS API calls
3070+
key = settings.api_key
3071+
logger.info(f'Accessing api_key triggers AWS Secrets Manager call: {key!r}')
3072+
logger.info('In production, this would fetch from AWS Secrets Manager')
3073+
except Exception as e:
3074+
# In CI/test environments without AWS credentials, initialization fails
3075+
logger.info(f'Would work with AWS credentials: {type(e).__name__}')
3076+
```
3077+
3078+
#### Azure Key Vault
3079+
3080+
```py
3081+
import logging
3082+
3083+
from azure.identity import DefaultAzureCredential
3084+
from pydantic import Field
3085+
3086+
from pydantic_settings import (
3087+
AzureKeyVaultSettingsSource,
3088+
BaseSettings,
3089+
PydanticBaseSettingsSource,
3090+
)
3091+
3092+
logging.basicConfig(level=logging.INFO)
3093+
logger = logging.getLogger(__name__)
3094+
3095+
3096+
class Settings(BaseSettings):
3097+
api_key: str = Field(validation_alias='MyApiKey')
3098+
database_password: str = Field(validation_alias='DbPassword')
3099+
3100+
@classmethod
3101+
def settings_customise_sources(
3102+
cls,
3103+
settings_cls: type[BaseSettings],
3104+
init_settings: PydanticBaseSettingsSource,
3105+
env_settings: PydanticBaseSettingsSource,
3106+
dotenv_settings: PydanticBaseSettingsSource,
3107+
file_secret_settings: PydanticBaseSettingsSource,
3108+
) -> tuple[PydanticBaseSettingsSource, ...]:
3109+
azure_keyvault = AzureKeyVaultSettingsSource(
3110+
settings_cls,
3111+
vault_url='https://myvault.vault.azure.net/',
3112+
credential=DefaultAzureCredential(),
3113+
lazy_load=True,
3114+
)
3115+
return (
3116+
init_settings,
3117+
env_settings,
3118+
dotenv_settings,
3119+
azure_keyvault,
3120+
file_secret_settings,
3121+
)
3122+
3123+
3124+
# Quick initialization - no Azure API calls made
3125+
try:
3126+
settings = Settings()
3127+
logger.info('Settings initialized instantly (no Azure calls yet)')
3128+
# Fields resolved on first access (would call Azure API in production)
3129+
key = settings.api_key
3130+
logger.info(f'Accessing api_key triggers Azure call: {key!r}')
3131+
except Exception as e:
3132+
# In CI/test environments without Azure credentials, initialization fails
3133+
logger.info(f'Would work with Azure credentials: {type(e).__name__}')
3134+
```
3135+
3136+
#### Google Cloud Secret Manager
3137+
3138+
```py
3139+
import logging
3140+
import os
3141+
3142+
from pydantic import Field
3143+
3144+
from pydantic_settings import (
3145+
BaseSettings,
3146+
GoogleSecretManagerSettingsSource,
3147+
PydanticBaseSettingsSource,
3148+
)
3149+
3150+
logging.basicConfig(level=logging.INFO)
3151+
logger = logging.getLogger(__name__)
3152+
3153+
3154+
class Settings(BaseSettings):
3155+
api_key: str = Field(validation_alias='MyApiKey')
3156+
database_password: str = Field(validation_alias='DbPassword')
3157+
3158+
@classmethod
3159+
def settings_customise_sources(
3160+
cls,
3161+
settings_cls: type[BaseSettings],
3162+
init_settings: PydanticBaseSettingsSource,
3163+
env_settings: PydanticBaseSettingsSource,
3164+
dotenv_settings: PydanticBaseSettingsSource,
3165+
file_secret_settings: PydanticBaseSettingsSource,
3166+
) -> tuple[PydanticBaseSettingsSource, ...]:
3167+
gcp_secrets = GoogleSecretManagerSettingsSource(
3168+
settings_cls,
3169+
project_id=os.environ.get('GCP_PROJECT_ID', 'my-project'),
3170+
lazy_load=True,
3171+
)
3172+
return (
3173+
init_settings,
3174+
env_settings,
3175+
dotenv_settings,
3176+
gcp_secrets,
3177+
file_secret_settings,
3178+
)
3179+
3180+
3181+
# Fast initialization - no GCP API calls made
3182+
try:
3183+
settings = Settings()
3184+
logger.info('Settings initialized instantly (no GCP calls yet)')
3185+
# First access would trigger GCP Secret Manager API call (in production)
3186+
key = settings.api_key
3187+
logger.info(f'Accessing api_key triggers GCP call: {key!r}')
3188+
except Exception as e:
3189+
# In CI/test environments without GCP credentials, initialization fails
3190+
logger.info(f'Would work with GCP credentials: {type(e).__name__}')
3191+
```
3192+
3193+
### Behavior and Caching
3194+
3195+
When lazy loading is enabled:
3196+
3197+
1. **Initialization**: Settings are created with minimal overhead. Sources return empty dictionaries instead of eagerly fetching all values.
3198+
3199+
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.
3200+
3201+
3. **Subsequent Access**: Accessing the same field again returns the cached value without making another API call.
3202+
3203+
4. **All Fields**: Iteration over all fields (via `model_dump()`, etc.) will trigger resolution of all fields at once.
3204+
3205+
```py
3206+
import logging
3207+
3208+
from pydantic_settings import (
3209+
AWSSecretsManagerSettingsSource,
3210+
BaseSettings,
3211+
PydanticBaseSettingsSource,
3212+
)
3213+
3214+
logging.basicConfig(level=logging.INFO)
3215+
logger = logging.getLogger(__name__)
3216+
3217+
3218+
class Settings(BaseSettings):
3219+
secret1: str
3220+
secret2: str
3221+
3222+
@classmethod
3223+
def settings_customise_sources(
3224+
cls,
3225+
settings_cls: type[BaseSettings],
3226+
init_settings: PydanticBaseSettingsSource,
3227+
env_settings: PydanticBaseSettingsSource,
3228+
dotenv_settings: PydanticBaseSettingsSource,
3229+
file_secret_settings: PydanticBaseSettingsSource,
3230+
) -> tuple[PydanticBaseSettingsSource, ...]:
3231+
aws_settings = AWSSecretsManagerSettingsSource(
3232+
settings_cls, secret_id='my-secret', lazy_load=True
3233+
)
3234+
return (
3235+
init_settings,
3236+
env_settings,
3237+
dotenv_settings,
3238+
aws_settings,
3239+
file_secret_settings,
3240+
)
3241+
3242+
3243+
try:
3244+
settings = Settings()
3245+
logger.info('Settings initialized with lazy loading enabled')
3246+
# Only secret1 is fetched from AWS Secrets Manager
3247+
s1 = settings.secret1
3248+
logger.info(f'Accessing secret1 triggers AWS API call: {s1!r}')
3249+
# secret1 is already cached, so this doesn't trigger another API call
3250+
logger.info('Accessing secret1 again uses cached value')
3251+
# secret2 is fetched on first access
3252+
s2 = settings.secret2
3253+
logger.info(f'Accessing secret2 triggers AWS API call: {s2!r}')
3254+
# This triggers fetching of all remaining fields not yet accessed
3255+
all_values = settings.model_dump()
3256+
logger.info('All fields fetched with model_dump()')
3257+
except Exception as e:
3258+
# In CI/test environments without AWS credentials, initialization fails
3259+
# In production with proper AWS credentials, Settings() would succeed instantly
3260+
err_type = type(e).__name__
3261+
logger.info(f'Settings initialization would work with AWS credentials: {err_type}')
3262+
```

0 commit comments

Comments
 (0)