pydantic-settings icon indicating copy to clipboard operation
pydantic-settings copied to clipboard

Strictness with BaseSettings

Open pycaw opened this issue 4 months ago • 4 comments

I fail to be able to use strict parsing with BaseSettings. I haven't found this behavior documented in the docs whatsoever. No mention of strict/strictness among the issues here either as far as I could see.

Given these definitions,

import json as json

from pydantic import BaseModel, StrictBool, StrictInt, ValidationError
from pydantic_settings import BaseSettings

json_doc = """
{
    "mybool": 0
}
"""


class Model(BaseModel):
    mybool: bool


class Settings(BaseSettings):
    mybool: bool


class StrictSettings(BaseSettings):
    mybool: StrictBool

we will get ValidationErrors with Model but not with Settings:

try:
    data = Model.model_validate_json(json_doc, strict=True)
except ValidationError as e:
    assert e.error_count() == 1

data = Settings.model_validate_json(json_doc, strict=True)

Same with model_validate. Even enabling strict on model_config doesn't have an effect

Settings.model_config["strict"] = True

obj = json.loads(json_doc)

try:
    data = Model.model_validate(obj, strict=True)
except ValidationError as e:
    assert e.error_count() == 1

data = Settings.model_validate(obj, strict=True)

Using Strict* types changes things but that's not what I am looking for:

try:
    data = StrictSettings.model_validate(obj)
except ValidationError as e:
    assert e.error_count() == 1

My use case is that I want CLI arguments to be parsed normally and config files to be parsed strictly.

And I use yaml config files so that will be another problem with SettingsSource configuration but let's not get ahead of ourselves.

pycaw avatar Sep 03 '25 13:09 pycaw

The more I think about the more it seems that model_validate* should not be used with BaseSettings. All examples construct BaseSettings instances by relying on the various sources. It is weird however why model_validate* methods sit there with options like strict: bool waiting to be misused. I only resorted to this way of doing it so that I can produce simplified examples. As I mentioned my use case revolves around CLI argument and config file parsing. I had gotten here from this test of mine where I tried to hack in strict parsing just for config files:

from pydantic_settings import BaseSettings, JsonConfigSettingsSource, PydanticBaseSettingsSource

class Settings(BaseSettings):
    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        strict = True
        json = True

        if strict:
            # Doesn't quite help
            StrictBase = type("StrictBase", (settings_cls,), {})
            StrictBase.model_config["strict"] = True
            # or this, since I noticed altering model_config members late don't work
            StrictBase = type(
                "StrictBase", (settings_cls,), {"model_config": SettingsConfigDict(strict=True)}
            )

            base = StrictBase
        else:
            base = settings_cls

        if json:
            return (JsonConfigSettingsSource(base, json_file="myconfig.json"),)
        else:
            return (YamlConfigSettingsSource(base, yaml_file="myconfig.yaml"),)

pycaw avatar Sep 03 '25 15:09 pycaw

Thanks @pycaw for this issue.

pydantic-settings overrides the BaseModel.__init__ and for some reason, in model_validate and model_validate_json function call the init function of BaseSettings will be called with the values, and it will be passed to the BaseModel.__init__. So, when you call Settings.model_validate(obj, strict=True) it actually will be call BaseModel.__init__(**obj). That's why pydantic-settings ignores the strict flag.

@Viicos Do you have more to add here? is there a way to fix the problem without a refactor/breaking change?

hramezani avatar Sep 03 '25 15:09 hramezani

My recently created issue about problem X: When using CLI parsing, how to apply strict parsing for other sources?

pycaw avatar Sep 03 '25 16:09 pycaw

Unfortunately the custom __init__() from BaseSettings isn't playing well (Pydantic issue tracking this: https://github.com/pydantic/pydantic/issues/10062).

Even enabling strict on model_config doesn't have an effect

Configuration isn't meant to be mutated directly (https://github.com/pydantic/pydantic/issues/12372), it will work if you set strict=True at model definition.

Viicos avatar Oct 26 '25 08:10 Viicos