9.3 KiB
name, description, x-personal-mcp
| name | description | x-personal-mcp | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pydantic-settings | Practical guide for implementing typed application configuration with pydantic-settings. Use when designing BaseSettings models, choosing env naming strategy, configuring dotenv or secrets, and customizing source priority safely. |
|
Pydantic Settings Implementation Guide
Use this skill to implement robust, typed application configuration with pydantic-settings in production Python services.
When to Use
- You need a single typed configuration model for app settings.
- You are migrating from ad-hoc
os.getenv(...)calls. - You need predictable precedence across init args, env vars, dotenv files, and secrets.
- You need nested settings models and reliable parsing behavior.
- You need to customize settings sources or source order safely.
Procedure
1. Baseline Model
Create a single settings model for the service boundary:
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseSettings(BaseModel):
host: str = "localhost"
port: int = 5432
user: str
password: str
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
debug: bool = False
log_level: str = "info"
database: DatabaseSettings
api_key: str = Field(validation_alias="MY_API_KEY")
Quality gate:
- Required fields fail fast when missing.
- Defaults are intentional and safe.
2. Pick Env Naming Rules
- Choose one prefix and apply it consistently.
- Use aliases only for compatibility or external contracts.
- Document whether env names are case-sensitive.
Quality gate:
- Team can derive env variable names without guessing.
- Legacy names are supported only where needed.
3. Decide Nested Parsing
For nested models via env vars, configure delimiters intentionally:
model_config = SettingsConfigDict(
env_prefix="APP_",
env_nested_delimiter="__",
env_nested_max_split=1,
)
Typical vars:
APP_DATABASE={"host": "db", "port": 5432, "user": "svc", "password": "pw"}APP_DATABASE__HOST=db.internal
Quality gate:
- Nested overrides behave as expected.
- Delimiter choice does not collide with field names.
4. Confirm Source Priority
Default priority (higher first):
- CLI args (if enabled)
- init kwargs
- env vars
- dotenv
- secrets dir
- defaults
Only customize when required:
from pydantic_settings import PydanticBaseSettingsSource
@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, ...]:
return (init_settings, env_settings, dotenv_settings, file_secret_settings)
Quality gate:
- Priority order is explicit in code.
- Tests verify conflict resolution.
5. Add Secrets Strategy
- In local development, dotenv is acceptable for non-production values.
- In deployed environments, prefer env vars or secret managers.
- For file-mounted secrets, use
secrets_dir.
Example:
model_config = SettingsConfigDict(
env_prefix="APP_",
env_file=".env",
secrets_dir="/run/secrets",
)
Quality gate:
- No secret literals in repository code.
- Missing secrets behavior is understood per environment.
6. Add ContextVar-Scoped Constructors And Accessors
When configuration and database resources should be request- or context-scoped, use ContextVar backed constructor and accessor methods.
Example pattern:
from contextlib import contextmanager
from contextvars import ContextVar
from functools import cache
from pydantic import SecretStr
from pydantic_settings import BaseSettings
from sqlmodel import Session, create_engine
from sqlalchemy import Engine
class DbSettings(BaseSettings):
model_config = {
"env_prefix": "DB_",
"extra": "ignore",
}
host: str = "localhost"
port: int = 5432
username: str
password: SecretStr
@property
def dsn(self) -> str:
return (
"postgresql://"
f"{self.username}:{self.password.get_secret_value()}"
f"@{self.host}:{self.port}/mydatabase"
)
_db_settings: ContextVar[DbSettings | None] = ContextVar("db_settings", default=None)
_db_conn: ContextVar[Engine | None] = ContextVar("db_conn", default=None)
def get_db_settings(**kwargs) -> DbSettings:
settings = _db_settings.get()
if settings is None:
settings = DbSettings(**kwargs)
_db_settings.set(settings)
cleanup_engine()
return settings
@cache
def get_db_engine() -> Engine:
engine = _db_conn.get()
if engine is None:
engine = create_engine(get_db_settings().dsn)
_db_conn.set(engine)
return engine
def cleanup_engine() -> None:
engine = _db_conn.get()
if engine is not None:
engine.dispose()
_db_conn.set(None)
get_db_engine.cache_clear()
@contextmanager
def get_session():
with Session(get_db_engine()) as session:
yield session
Design notes:
get_db_settingsis the constructor/accessor for settings and can accept explicit overrides in tests.get_db_engineis the constructor/accessor for the engine and reuses context-local state.cleanup_enginemust run when settings change so stale DSNs do not leak across contexts.get_sessioncentralizes session creation so call sites never build engines directly.
Quality gate:
- Overriding settings triggers engine cleanup and cache invalidation.
- No module-level global engine is created outside accessors.
- Session creation always goes through
get_session().
7. Add Focused Resource-Lifecycle Test
Do not add tests that re-validate baseline pydantic-settings functionality (for example env parsing, alias semantics, or source precedence) unless you have custom behavior layered on top.
Minimum test to add (only when an engine accessor exists):
- assert the database engine is not instantiated more than once for repeated accessor calls in the same lifecycle/context
If the project has no database engine accessor, skip this section.
Suggested invocation:
uv run pytest -q
Completion Checks
- A single typed settings model exists for the service boundary.
- Source precedence is documented and tested.
- Env naming conventions and aliases are explicit and stable.
- Nested parsing behavior is tested when custom parsing behavior is added.
- Secrets and dotenv usage are environment-appropriate and do not leak sensitive defaults.
- Validation errors are actionable and fail fast for required values.
- If an engine accessor exists, engine construction occurs at most once per lifecycle/context.
Output Contract
When this skill is applied, return:
- Which references were consulted.
- The chosen source-precedence model and why.
- The exact parsing and alias decisions made.
- Any deferred choices and their risk.
- The validation commands or tests run to confirm behavior.
Use these upstream docs when implementing or reviewing pydantic-settings behavior.
Source Docs
Primary
Core Concepts
Priority And Sources
Environment And Parsing
- Environment variable names and prefix behavior
- Case sensitivity behavior
- Parsing environment variable values
- Nested model default partial updates