@@ -100,6 +100,7 @@ class SettingsConfigDict(ConfigDict, total=False):
100100
101101 toml_file : PathType | None
102102 enable_decoding : bool
103+ lazy_load : bool
103104
104105
105106# Extend `config_keys` by pydantic settings config keys to
@@ -159,6 +160,8 @@ class BaseSettings(BaseModel):
159160 _cli_kebab_case: CLI args use kebab case. Defaults to `False`.
160161 _cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`.
161162 _secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`.
163+ _lazy_load: Defer field value resolution until fields are accessed. When enabled, field values
164+ are only fetched from the source when explicitly accessed, not during settings initialization.
162165 """
163166
164167 def __init__ (
@@ -191,6 +194,10 @@ def __init__(
191194 _secrets_dir : PathType | None = None ,
192195 ** values : Any ,
193196 ) -> None :
197+ # Temp storage for lazy sources collected during _settings_build_values
198+ _temp_lazy_sources : dict [str , Any ] = {}
199+ __pydantic_self__ ._temp_lazy_sources = _temp_lazy_sources
200+
194201 super ().__init__ (
195202 ** __pydantic_self__ ._settings_build_values (
196203 values ,
@@ -223,6 +230,117 @@ def __init__(
223230 )
224231 )
225232
233+ # Now that super().__init__() has completed, set the lazy sources on the instance
234+ # using object.__setattr__ to bypass any Pydantic restrictions
235+ object .__setattr__ (__pydantic_self__ , '_lazy_sources' , _temp_lazy_sources )
236+
237+ def __getattribute__ (self , name : str ) -> Any :
238+ """Intercept field access to support lazy loading on demand."""
239+ # Get the actual value from the model
240+ value = super ().__getattribute__ (name )
241+
242+ # Return private attributes and methods as-is
243+ if name .startswith ('_' ) or callable (value ):
244+ return value
245+
246+ # For model fields, try to get value from lazy sources only if not set by
247+ # higher-priority sources. We detect this by checking if the value is the field's default.
248+ try :
249+ model_cls = type (self )
250+ if name in model_cls .model_fields :
251+ field_info = model_cls .model_fields [name ]
252+ # Only try lazy sources if the value is the default (wasn't set by higher-priority source)
253+ # Check if value is the field's default value
254+ is_default = False
255+ if field_info .is_required ():
256+ # Required fields have no default, so if value is not None, it was set
257+ is_default = value is None
258+ elif field_info .default is not None :
259+ is_default = value == field_info .default
260+ elif field_info .default_factory is not None :
261+ # For fields with default_factory, comparing to the factory output
262+ # would require calling the factory, so we check if value is unset
263+ is_default = value is None or value == field_info .default
264+ else :
265+ is_default = value is None or value == field_info .default
266+
267+ if is_default :
268+ lazy_sources = object .__getattribute__ (self , '_lazy_sources' )
269+ for lazy_mapping in lazy_sources .values ():
270+ try :
271+ return lazy_mapping [name ]
272+ except KeyError :
273+ pass
274+ except AttributeError :
275+ pass
276+
277+ return value
278+
279+ def model_dump (
280+ self ,
281+ * ,
282+ mode : str | Literal ['json' , 'python' ] = 'python' ,
283+ include : Any = None ,
284+ exclude : Any = None ,
285+ context : Any = None ,
286+ by_alias : bool | None = None ,
287+ exclude_unset : bool = False ,
288+ exclude_defaults : bool = False ,
289+ exclude_none : bool = False ,
290+ round_trip : bool = False ,
291+ warnings : Literal ['none' , 'warn' , 'error' ] | bool = True ,
292+ ** kwargs : Any ,
293+ ) -> dict [str , Any ]:
294+ """Override model_dump to include cached lazy-loaded values."""
295+ # Get base dump from parent class
296+ dump = super ().model_dump (
297+ mode = mode ,
298+ include = include ,
299+ exclude = exclude ,
300+ context = context ,
301+ by_alias = by_alias ,
302+ exclude_unset = exclude_unset ,
303+ exclude_defaults = exclude_defaults ,
304+ exclude_none = exclude_none ,
305+ round_trip = round_trip ,
306+ warnings = warnings ,
307+ ** kwargs ,
308+ )
309+
310+ # Merge lazy values from _lazy_sources, triggering loads if needed
311+ try :
312+ lazy_sources = object .__getattribute__ (self , '_lazy_sources' )
313+ for source_name , lazy_mapping in lazy_sources .items ():
314+ # Iterate through all fields in the lazy mapping and load any that are
315+ # still at their default value (not set by higher-priority sources)
316+ for field_name in lazy_mapping :
317+ # Check if field is still at default in dump
318+ if field_name in type (self ).model_fields :
319+ field_info = type (self ).model_fields [field_name ]
320+ current_value = dump .get (field_name )
321+
322+ # Determine if this is still a default value
323+ is_default = False
324+ if field_info .is_required ():
325+ is_default = current_value is None
326+ elif field_info .default is not None :
327+ is_default = current_value == field_info .default
328+ else :
329+ is_default = current_value is None or current_value == field_info .default
330+
331+ # If still at default, try to load from lazy mapping
332+ if is_default :
333+ try :
334+ dump [field_name ] = lazy_mapping [field_name ]
335+ except KeyError :
336+ # Field not available in this lazy source, keep default
337+ pass
338+ except AttributeError :
339+ # _lazy_sources not set (no lazy sources configured) - return base dump
340+ pass
341+
342+ return dump
343+
226344 @classmethod
227345 def settings_customise_sources (
228346 cls ,
@@ -437,6 +555,11 @@ def _settings_build_values(
437555 source_name = source .__name__ if hasattr (source , '__name__' ) else type (source ).__name__
438556 source_state = source ()
439557
558+ # Collect lazy mappings from sources for later field access
559+ if hasattr (source , '_lazy_mapping' ):
560+ temp_lazy_sources = self ._temp_lazy_sources
561+ temp_lazy_sources [source_name ] = source ._lazy_mapping
562+
440563 if isinstance (source , DefaultSettingsSource ):
441564 defaults = source_state
442565
0 commit comments