pydantic-settings update
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: pydantic-settings
|
||||
description: "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."
|
||||
description: "Practical guide for implementing typed application configuration with pydantic-settings. Use when designing BaseSettings models, choosing nested or independent settings boundaries, managing settings lifecycles, configuring dotenv or secrets, and customizing source priority safely."
|
||||
x-personal-mcp:
|
||||
id: pydantic-settings
|
||||
version: 1.0.0
|
||||
version: 1.1.0
|
||||
tags:
|
||||
- python
|
||||
- pydantic
|
||||
@@ -13,6 +13,8 @@ x-personal-mcp:
|
||||
- secrets
|
||||
- dotenv
|
||||
- source-priority
|
||||
- caching
|
||||
- lifecycle
|
||||
capabilities:
|
||||
- resource://skills/pydantic-settings/document
|
||||
---
|
||||
@@ -27,6 +29,8 @@ Use this skill to implement robust, typed application configuration with `pydant
|
||||
- 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 choose between one nested application settings object and independently owned settings objects.
|
||||
- You need a deliberate construction, caching, or reload lifecycle.
|
||||
- You need to customize settings sources or source order safely.
|
||||
|
||||
## Procedure
|
||||
@@ -53,6 +57,7 @@ class Settings(BaseSettings):
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
frozen=True,
|
||||
)
|
||||
|
||||
debug: bool = False
|
||||
@@ -154,101 +159,122 @@ Quality gate:
|
||||
1. No secret literals in repository code.
|
||||
2. Missing secrets behavior is understood per environment.
|
||||
|
||||
### 6. Add ContextVar-Scoped Constructors And Accessors
|
||||
### 6. Choose Nested Or Independent Settings Boundaries
|
||||
|
||||
When configuration and database resources should be request- or context-scoped, use `ContextVar` backed constructor and accessor methods.
|
||||
|
||||
Example pattern:
|
||||
Prefer one root `BaseSettings` object with nested `BaseModel` sections when the configuration belongs to one application lifecycle:
|
||||
|
||||
```python
|
||||
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
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class DbSettings(BaseSettings):
|
||||
model_config = {
|
||||
"env_prefix": "DB_",
|
||||
"extra": "ignore",
|
||||
}
|
||||
|
||||
class DatabaseSettings(BaseModel):
|
||||
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)
|
||||
class ObservabilitySettings(BaseModel):
|
||||
log_level: str = "INFO"
|
||||
json_logs: bool = True
|
||||
|
||||
|
||||
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
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="APP_",
|
||||
env_nested_delimiter="__",
|
||||
frozen=True,
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
database: DatabaseSettings = Field(default_factory=DatabaseSettings)
|
||||
observability: ObservabilitySettings = Field(
|
||||
default_factory=ObservabilitySettings
|
||||
)
|
||||
```
|
||||
|
||||
Design notes:
|
||||
This produces names such as `APP_DATABASE__HOST` and gives the application one validated, atomic configuration snapshot. Nested sections should normally inherit from `BaseModel`, not `BaseSettings`; otherwise each nested settings model can collect sources independently and produce surprising results.
|
||||
|
||||
1. `get_db_settings` is the constructor/accessor for settings and can accept explicit overrides in tests.
|
||||
2. `get_db_engine` is the constructor/accessor for the engine and reuses context-local state.
|
||||
3. `cleanup_engine` must run when settings change so stale DSNs do not leak across contexts.
|
||||
4. `get_session` centralizes session creation so call sites never build engines directly.
|
||||
Use independent `BaseSettings` classes when the objects have genuinely independent ownership:
|
||||
|
||||
1. Different packages or deployable components own the schemas.
|
||||
2. Each object needs its own env prefix or source policy.
|
||||
3. A component is optional or loaded lazily.
|
||||
4. Components need different reload lifecycles.
|
||||
5. The same component must run outside the application.
|
||||
|
||||
Construct independent objects explicitly at the composition root and inject each dependency. Do not nest one `BaseSettings` class inside another merely to reuse its fields. Extract a shared `BaseModel` schema when models need common structure.
|
||||
|
||||
Quality gate:
|
||||
|
||||
1. Overriding settings triggers engine cleanup and cache invalidation.
|
||||
2. No module-level global engine is created outside accessors.
|
||||
3. Session creation always goes through `get_session()`.
|
||||
1. Nested sections share one source policy and lifecycle.
|
||||
2. Independent settings have distinct owners, prefixes, or lifecycles.
|
||||
3. The application does not repeatedly scan the same sources through accidental nested `BaseSettings` construction.
|
||||
|
||||
### 7. Add Focused Resource-Lifecycle Test
|
||||
### 7. Own The Settings Lifecycle
|
||||
|
||||
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.
|
||||
For most applications, construct settings once at the composition root and pass the validated object to services:
|
||||
|
||||
Minimum test to add (only when an engine accessor exists):
|
||||
```python
|
||||
def main() -> None:
|
||||
settings = Settings()
|
||||
application = Application(settings=settings)
|
||||
application.run()
|
||||
```
|
||||
|
||||
1. assert the database engine is not instantiated more than once for repeated accessor calls in the same lifecycle/context
|
||||
This makes ownership, startup failure, and test overrides explicit. Treat the object as a snapshot: environment variables and files changing later do not update an existing instance. Prefer `frozen=True` for shared settings so consumers cannot silently mutate process-wide configuration.
|
||||
|
||||
If the project has no database engine accessor, skip this section.
|
||||
Use [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) only when process-lifetime singleton access is intentional and explicit injection is awkward, such as a framework dependency provider:
|
||||
|
||||
```python
|
||||
from functools import cache
|
||||
|
||||
|
||||
@cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
```
|
||||
|
||||
Keep the cached factory argument-free. Passing override kwargs creates one cached instance per argument combination, retains those values for the process lifetime, and obscures which configuration is active. In tests, instantiate `Settings(...)` directly or override the dependency; when a test must exercise the cached getter, isolate environment changes with `get_settings.cache_clear()` before and after the assertion.
|
||||
|
||||
`cache` is process-local. Every worker process gets its own instance, and concurrent first calls can construct more than one instance before the cache is populated. Settings construction must therefore be side-effect free; create engines, clients, and sessions in their own lifecycle-managed providers.
|
||||
|
||||
Quality gate:
|
||||
|
||||
1. Settings are created once per intended application or worker lifecycle.
|
||||
2. Cached factories are argument-free and side-effect free.
|
||||
3. Tests do not leak cached settings or environment changes.
|
||||
4. Resource construction is separate from configuration parsing.
|
||||
|
||||
### 8. Reload Deliberately
|
||||
|
||||
Static service configuration should normally require a process restart. If runtime reload is a real requirement, construct a fresh settings instance and atomically replace the owned reference. Do not call `__init__()` on a shared instance: readers can observe mutation in progress, and resources derived from old values may remain alive.
|
||||
|
||||
Settings sources are synchronous. In an async application, construction or reload that reads dotenv, secrets, JSON, TOML, or YAML files should run in a worker thread:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
|
||||
async def load_settings() -> Settings:
|
||||
return await asyncio.to_thread(Settings)
|
||||
```
|
||||
|
||||
Clearing `get_settings` is sufficient for controlled tests or single-threaded administration, but it is not an atomic live-reload protocol. Concurrent applications should own the current reference behind an application-specific lock or lifecycle manager, swap in a fully validated replacement, and then rebuild dependent resources.
|
||||
|
||||
Quality gate:
|
||||
|
||||
1. Reload creates and validates a replacement before publication.
|
||||
2. Readers cannot observe a partially mutated object.
|
||||
3. Dependent resources are recreated after the settings reference changes.
|
||||
4. File-backed source reads do not block an async event loop.
|
||||
|
||||
### 9. Add Focused Lifecycle Tests
|
||||
|
||||
Do not add tests that re-validate baseline `pydantic-settings` functionality unless custom behavior is layered on top. Test the application-owned behavior instead:
|
||||
|
||||
1. Repeated cached getter calls return the same instance.
|
||||
2. Cache clearing after an environment change returns a newly validated instance.
|
||||
3. Explicitly injected settings bypass global cached state.
|
||||
4. Reload swaps the settings snapshot and rebuilds dependent resources, when reload is supported.
|
||||
|
||||
Suggested invocation:
|
||||
|
||||
@@ -256,13 +282,15 @@ Suggested invocation:
|
||||
|
||||
## Completion Checks
|
||||
|
||||
1. A single typed settings model exists for the service boundary.
|
||||
1. Settings ownership matches the application or component lifecycle.
|
||||
2. Source precedence is documented and tested.
|
||||
3. Env naming conventions and aliases are explicit and stable.
|
||||
4. Nested parsing behavior is tested when custom parsing behavior is added.
|
||||
5. Secrets and dotenv usage are environment-appropriate and do not leak sensitive defaults.
|
||||
6. Validation errors are actionable and fail fast for required values.
|
||||
7. If an engine accessor exists, engine construction occurs at most once per lifecycle/context.
|
||||
7. Cached factories are argument-free, process-local, and cleared deliberately in tests.
|
||||
8. Nested models share one source policy; independent settings have an explicit ownership reason.
|
||||
9. Runtime reload, if supported, replaces a validated snapshot and rebuilds dependent resources.
|
||||
|
||||
## Output Contract
|
||||
|
||||
@@ -303,6 +331,12 @@ Use these upstream docs when implementing or reviewing `pydantic-settings` behav
|
||||
- [Parsing environment variable values](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#parsing-environment-variable-values)
|
||||
- [Nested model default partial updates](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#nested-model-default-partial-updates)
|
||||
|
||||
### Lifecycle And Reloading
|
||||
|
||||
- [In-place reloading](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#in-place-reloading)
|
||||
- [Async environments](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#async-environments)
|
||||
- [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)
|
||||
|
||||
### Dotenv And Secrets
|
||||
|
||||
- [Dotenv support](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#dotenv-env-support)
|
||||
|
||||
Reference in New Issue
Block a user