-
-
Notifications
You must be signed in to change notification settings - Fork 104
Open
Labels
Description
Hi, I was trying to use nested_model_default_partial_update=True
with exclude_unset=True
(in part to confirm which arguments were explicitly set by the CLI user, vs just the defaults). I noticed some slightly unexpected behavior, which was that nested_model_default_partial_update=True
made all nested models appear as though all their fields were set.
Here is a script to reproduce:
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pydantic",
# "pydantic-settings",
# ]
# ///
"""
Minimal example demonstrating bug with nested_model_default_partial_update=True
and exclude_unset=True in pydantic-settings.
Bug: When nested_model_default_partial_update=True, exclude_unset=True behaves
incorrectly - it thinks every field is set even when they're not.
Run with:
uv run --script minimal_pydantic_bug_example.py
"""
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
class NestedModel(BaseModel):
field_a: str = "default_a"
field_b: int = 42
class SettingsWithPartialUpdate(BaseSettings):
model_config = SettingsConfigDict(
nested_model_default_partial_update=True,
)
nested: NestedModel = NestedModel()
simple_field: str = "default_simple"
class SettingsWithoutPartialUpdate(BaseSettings):
model_config = SettingsConfigDict(
nested_model_default_partial_update=False,
)
nested: NestedModel = NestedModel()
simple_field: str = "default_simple"
# Test without nested_model_default_partial_update
print("1. WITHOUT nested_model_default_partial_update:")
settings_without = SettingsWithoutPartialUpdate()
dumped_without = settings_without.model_dump(exclude_unset=True)
print(f" exclude_unset=True result: {dumped_without}")
print(" Expected: empty dict (no fields explicitly set)")
print(f" Is empty? {len(dumped_without) == 0}")
print()
# Test with nested_model_default_partial_update
print("2. WITH nested_model_default_partial_update:")
settings_with = SettingsWithPartialUpdate()
dumped_with = settings_with.model_dump(exclude_unset=True)
print(f" exclude_unset=True result: {dumped_with}")
print(" Expected: empty dict (no fields explicitly set)")
print(f" Is empty? {len(dumped_with) == 0}")
print()
# Show the actual model fields state
print("3. Debugging field state:")
print(
f" settings_without.nested.__pydantic_fields_set__: {settings_without.nested.__pydantic_fields_set__}"
)
print(
f" settings_with.nested.__pydantic_fields_set__: {settings_with.nested.__pydantic_fields_set__}"
)
print()
# Test with explicitly set values
print("4. Testing with explicitly set values:")
settings_explicit = SettingsWithPartialUpdate(
nested=NestedModel(field_a="explicit_value")
)
dumped_explicit = settings_explicit.model_dump(exclude_unset=True)
print(f" With explicit nested.field_a: {dumped_explicit}")
print(
f" nested.__pydantic_fields_set__: {settings_explicit.nested.__pydantic_fields_set__}"
)
Outputs:
❯ uv run --script minimal_pydantic_bug_example.py
1. WITHOUT nested_model_default_partial_update:
exclude_unset=True result: {}
Expected: empty dict (no fields explicitly set)
Is empty? True
2. WITH nested_model_default_partial_update:
exclude_unset=True result: {'nested': {'field_a': 'default_a', 'field_b': 42}}
Expected: empty dict (no fields explicitly set)
Is empty? False
3. Debugging field state:
settings_without.nested.__pydantic_fields_set__: set()
settings_with.nested.__pydantic_fields_set__: {'field_b', 'field_a'}
4. Testing with explicitly set values:
With explicit nested.field_a: {'nested': {'field_a': 'explicit_value', 'field_b': 42}}
nested.__pydantic_fields_set__: {'field_b', 'field_a'}