pydantic-settings nested BaseSettings with env_nested_delimiter: working versions (tested 2026-10-16)
Group configuration into nested Pydantic settings models and populate them from prefixed environment variables separated by a double underscore, tested fully offline.
passed6 passed · 0 failed2026-10-16 00:01 UTC- Status
- passed
- Last passed
- 2026-10-16 00:01 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38826982604
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pydantic-settings-nested-sources") (install), or see them on GitHub.
pydantic-settings==2.16.0 pytest==9.1.1
from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class HttpRetrySettings(BaseModel):
attempts: int = 3
backoff: float = 0.5
class HttpSettings(BaseModel):
base_url: str = "https://api.example.com"
timeout: float = 10.0
retries: HttpRetrySettings = HttpRetrySettings()
class DbSettings(BaseModel):
url: str = "sqlite:///app.db"
pool_size: int = 5
password: SecretStr = SecretStr("dev")
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_nested_delimiter="__",
case_sensitive=False,
extra="ignore",
)
debug: bool = False
features: list[str] = []
http: HttpSettings = HttpSettings()
db: DbSettings = DbSettings()
class AuthSettings(BaseModel):
token: str
class ServiceSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="SVC_", env_nested_delimiter="__")
auth: AuthSettings
import os
import pytest
from pydantic import ValidationError
from config import HttpRetrySettings, HttpSettings, ServiceSettings, Settings
@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
for key in list(os.environ):
if key.upper().startswith(("APP_", "SVC_")):
monkeypatch.delenv(key)
def test_defaults_are_nested_instances():
s = Settings()
assert isinstance(s.http, HttpSettings)
assert s.http.retries.attempts == 3
assert s.db.password.get_secret_value() == "dev"
def test_nested_env_overrides(monkeypatch):
monkeypatch.setenv("app_db__url", "sqlite:///prod.db")
monkeypatch.setenv("APP_DB__POOL_SIZE", "20")
monkeypatch.setenv("APP_DEBUG", "on")
s = Settings()
assert s.db.url == "sqlite:///prod.db"
assert s.db.pool_size == 20
assert s.debug is True
def test_partial_nested_override_keeps_sibling_defaults(monkeypatch):
monkeypatch.setenv("APP_HTTP__TIMEOUT", "2.5")
s = Settings()
assert s.http.timeout == 2.5
assert s.http.base_url == "https://api.example.com"
assert s.http.retries.attempts == 3
def test_deep_nested_delimiter(monkeypatch):
monkeypatch.setenv("APP_HTTP__RETRIES__ATTEMPTS", "9")
monkeypatch.setenv("APP_HTTP__RETRIES__BACKOFF", "1.5")
s = Settings()
assert isinstance(s.http.retries, HttpRetrySettings)
assert s.http.retries.attempts == 9
assert s.http.retries.backoff == 1.5
def test_complex_field_decoded_from_json(monkeypatch):
monkeypatch.setenv("APP_FEATURES", '["beta", "dark-mode"]')
s = Settings()
assert s.features == ["beta", "dark-mode"]
def test_required_nested_model_is_built_from_env(monkeypatch):
with pytest.raises(ValidationError):
ServiceSettings()
monkeypatch.setenv("SVC_AUTH__TOKEN", "s3cr3t")
assert ServiceSettings().auth.token == "s3cr3t"
[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.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Loading .env files with python-dotenv and pydantic-settings precedence rules
Typed app settings from .env files with pydantic-settings, covering source precedence, layered env files, extra-key rules and python-dotenv override behavior, tested offline with tmp_path files.
Pydantic Settings typed config from env and .env
Typed app configuration with pydantic-settings: env prefixes, nested settings, secrets and validation errors.
pydantic-settings v2 config from env, YAML and defaults with SecretStr
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.
beartype conf + pydantic model_validate runtime checks
Pair beartype boundary checks with pydantic model_validate, using BeartypeConf to align int/float coercion and route violations, and pin down the gotchas where the two guards disagree.