ProvenPin Install

pydantic-settings v2 config from env, YAML and defaults with SecretStr: working versions (tested 2026-10-15)

Nested pydantic-settings models loaded with precedence init > env > YAML > defaults, with env_nested_delimiter, SecretStr masking, and offline tests using tmp_path and monkeypatch.

passed5 passed · 0 failed2026-10-15 11:56 UTC
Status
passed
Last passed
2026-10-15 11:56 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions5
pydantic-settings@2.16.0pydantic@2.14.1python-dotenv@1.2.2pyyaml@6.0.3pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38730348195

The code

Exactly the files that passed. Your agent can pull them with get_recipe("pydantic-settings-yaml-secrets") (install), or see them on GitHub.

requirements.txt
pydantic-settings==2.16.0
pydantic==2.14.1
python-dotenv==1.2.2
pyyaml==6.0.3
pytest==9.1.1
settings.py
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field, SecretStr
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
    YamlConfigSettingsSource,
)

class DbSettings(BaseModel):
    url: str = "sqlite:///app.db"
    password: SecretStr = SecretStr("")

class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="APP_", env_nested_delimiter="__", extra="ignore",
    )
    debug: bool = False
    api_key: SecretStr = SecretStr("dev-key")
    # default_factory avoids sharing one mutable default instance across objects
    db: DbSettings = Field(default_factory=DbSettings)

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls,
        init_settings,
        env_settings,
        dotenv_settings,
        file_secret_settings,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        # first source wins: init > env > yaml > field defaults
        return (init_settings, env_settings, YamlConfigSettingsSource(settings_cls))

def load_settings(yaml_file: Path | None = None, **init: Any) -> AppSettings:
    # a subclass per call lets each test point the YAML source at its own file
    cls = type(
        "LoadedSettings",
        (AppSettings,),
        {"model_config": AppSettings.model_config | {"yaml_file": yaml_file}},
    )
    return cls(**init)
tests/test_settings.py
import os

import pytest

from settings import load_settings

@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
    # stop APP_* variables from the developer's shell leaking into tests
    for key in list(os.environ):
        if key.startswith("APP_"):
            monkeypatch.delenv(key)

@pytest.fixture
def yaml_file(tmp_path):
    p = tmp_path / "config.yaml"
    p.write_text(
        "debug: true\n"
        "api_key: yaml-key\n"
        "db:\n"
        "  url: postgresql://yaml/db\n"
        "  password: yaml-pass\n"
    )
    return p

def test_defaults_when_nothing_set():
    s = load_settings()
    assert s.debug is False
    assert s.db.url == "sqlite:///app.db"

def test_yaml_overrides_defaults(yaml_file):
    s = load_settings(yaml_file)
    assert s.debug is True
    assert s.db.url == "postgresql://yaml/db"

def test_env_beats_yaml_and_merges_nested(yaml_file, monkeypatch):
    monkeypatch.setenv("APP_DB__URL", "sqlite:///env.db")
    s = load_settings(yaml_file)
    assert s.db.url == "sqlite:///env.db"
    # sibling nested key from yaml survives: sources are deep-merged
    assert s.db.password.get_secret_value() == "yaml-pass"

def test_init_beats_env(yaml_file, monkeypatch):
    monkeypatch.setenv("APP_DEBUG", "true")
    s = load_settings(yaml_file, debug=False)
    assert s.debug is False

def test_secrets_are_masked(yaml_file):
    s = load_settings(yaml_file)
    assert s.api_key.get_secret_value() == "yaml-key"
    assert "yaml-pass" not in repr(s)
    assert "yaml-pass" not in str(s.model_dump(mode="json"))
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.