Skip to content

nested_model_default_partial_update=True makes all nested-fields appear set #678

@charlesbmi

Description

@charlesbmi

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'}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions