ProvenPin Install

Loading .env files with python-dotenv and pydantic-settings precedence rules: working versions (tested 2026-10-15)

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.

passed5 passed · 0 failed2026-10-15 13:25 UTC
Status
passed
Last passed
2026-10-15 13:25 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions4
python-dotenv@1.2.4pydantic-settings@2.16.0pydantic@2.14.1pytest@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 38741369695

The code

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

requirements.txt
python-dotenv==1.2.4
pydantic-settings==2.16.0
pydantic==2.14.1
pytest==9.1.1
settings.py
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

# Source priority (highest first): init kwargs, real env vars, env_file(s), defaults.
class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_", extra="ignore")

    database_url: str = "sqlite:///:memory:"
    debug: bool = False
    api_key: SecretStr | None = None
    workers: int = Field(default=1, ge=1)
tests/test_settings.py
import os

import pytest
from dotenv import dotenv_values, load_dotenv
from pydantic import ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict

from settings import AppSettings


@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
    for key in [k for k in os.environ if k.startswith("APP_")]:
        monkeypatch.delenv(key)
    yield
    # load_dotenv writes to os.environ directly, which monkeypatch does not undo
    for key in [k for k in os.environ if k.startswith("APP_")]:
        os.environ.pop(key)


def write(tmp_path, name, text):
    path = tmp_path / name
    path.write_text(text)
    return path


def test_precedence_kwargs_over_env_over_dotenv(tmp_path, monkeypatch):
    env = write(tmp_path, ".env", "APP_DEBUG=false\n")
    assert AppSettings(_env_file=env).debug is False

    monkeypatch.setenv("APP_DEBUG", "true")  # real env var beats the file
    assert AppSettings(_env_file=env).debug is True

    assert AppSettings(_env_file=env, debug=False).debug is False  # init kwargs beat all


def test_later_env_file_wins(tmp_path):
    base = write(tmp_path, "base.env", "APP_WORKERS=2\nAPP_DATABASE_URL=sqlite:///base.db\n")
    local = write(tmp_path, "local.env", "APP_WORKERS=8\n")
    s = AppSettings(_env_file=(base, local))
    assert s.workers == 8
    assert s.database_url == "sqlite:///base.db"


def test_invalid_value_raises(tmp_path):
    env = write(tmp_path, ".env", "APP_WORKERS=0\n")
    with pytest.raises(ValidationError):
        AppSettings(_env_file=env)


class Strict(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", extra="forbid")
    debug: bool = False


def test_typo_key_rejected_when_extra_forbidden(tmp_path):
    env = write(tmp_path, ".env", "APP_DEGUB=true\n")
    with pytest.raises(ValidationError):
        Strict(_env_file=env)


def test_load_dotenv_does_not_override_existing_env(tmp_path, monkeypatch):
    env = write(tmp_path, ".env", "APP_DEBUG=false\nAPP_NEW=1\n")
    monkeypatch.setenv("APP_DEBUG", "true")
    load_dotenv(env)  # override=False by default
    assert os.environ["APP_DEBUG"] == "true"
    assert os.environ["APP_NEW"] == "1"
    assert dotenv_values(env)["APP_DEBUG"] == "false"  # parsed, not exported
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.