pydantic-settings: Typed Env Config in Python (2026)
pydantic-settings: Typed Env Config in Python (2026) shows how to replace ad-hoc os.environ lookups with a BaseSettings subclass that reads .env, coerces types, validates Field constraints, supports an optional env_prefix, and exposes a clean model_dump() — without starting a server.
Pair typed settings with agent workflows via Pydantic AI v2, ship a no-build UI with FastAPI + HTMX, or map models with SQLAlchemy 2.0 ORM.
TL;DR
- Subclass
BaseSettings; setSettingsConfigDict(env_file=".env")to load dotenv automatically. - Use
Fieldfor defaults, bounds, and descriptions; bool/int/list values coerce from env strings. - Optional
env_prefix="MYAPP_"namespaces process env vars without colliding with other apps. - Call
model_dump()for a plain dict; invalid values raiseValidationErrorbefore your app starts. - Install with
pip install pydantic-settings(we tested2.15.0with pydantic2.13.5on Python 3.13.5).
Why pydantic-settings in 2026?
Every FastAPI, worker, and CLI process needs a database URL, API key, and debug flag. Reading os.getenv by hand means silent Nones, stringly-typed booleans, and no schema. pydantic-settings reuses Pydantic v2 validation so config fails fast at startup — the cheapest reliability win in Web Development.
| API | Role | Use in 2026 |
|---|---|---|
BaseSettings | Typed config class | App / worker / CLI bootstrap |
SettingsConfigDict | env_file, env_prefix | Dotenv + namespaced env |
Field / model_dump | Constraints + export | Validate then pass to DI |
Versions tested (2026-10-04)
- Python
3.13.5 pydantic2.13.5pydantic-settings2.15.0- Demo:
.envload,env_prefix, list parsing, validation error
python -m venv .venv && source .venv/bin/activate
pip install pydantic-settings==2.15.0
# create .env next to the script, then:
python settings_demo.py
1. BaseSettings + Field + .env
Save this as settings_demo.py next to a .env file. Fields map to uppercased env names by default (app_name ← APP_NAME).
import sys
from typing import Annotated
import pydantic
import pydantic_settings
from pydantic import Field, ValidationError, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
print("Python", sys.version.split()[0])
print("pydantic", pydantic.__version__)
print("pydantic-settings", pydantic_settings.__version__)
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
app_name: str = Field(default="MyApp")
debug: bool = False
api_key: Annotated[str, Field(min_length=8)]
database_url: str = "sqlite:///./app.db"
cors_origins: list[str] = Field(default_factory=list)
max_retries: int = Field(default=3, ge=0, le=10)
@field_validator("api_key")
@classmethod
def api_key_not_placeholder(cls, v: str) -> str:
if v.lower() in {"changeme", "secret", "test"}:
raise ValueError("api_key must not be a placeholder")
return v
settings = Settings()
print("app_name", settings.app_name)
print("debug", settings.debug, type(settings.debug).__name__)
print("cors_origins", settings.cors_origins)
print("model_dump_keys", sorted(settings.model_dump().keys()))
Example .env used in the real run:
APP_NAME=PyInns Demo API
DEBUG=true
API_KEY=sk-demo-abc123xyz
DATABASE_URL=postgresql://user:pass@localhost:5432/app
CORS_ORIGINS=["http://localhost:3000","https://app.example.com"]
MAX_RETRIES=3
DEBUG=true becomes a real bool; JSON-looking list strings become list[str]. Missing required fields (or a too-short api_key) raise before you touch the network.
2. env_prefix for namespaced process env
When several services share one shell, prefix every variable:
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
os.environ["MYAPP_APP_NAME"] = "Prefixed Service"
os.environ["MYAPP_DEBUG"] = "0"
os.environ["MYAPP_API_KEY"] = "sk-prefixed-key99"
os.environ["MYAPP_MAX_RETRIES"] = "5"
class PrefixedSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="MYAPP_", extra="ignore")
app_name: str = "fallback"
debug: bool = True
api_key: str
max_retries: int = 1
pref = PrefixedSettings()
print("prefixed_app_name", pref.app_name)
print("prefixed_debug", pref.debug)
print("prefixed_max_retries", pref.max_retries)
3. Validation error before startup
try:
Settings(api_key="short", app_name="Bad")
except ValidationError as exc:
err = exc.errors()[0]
print("error_type", err["type"])
print("error_loc", err["loc"])
print("validation_failed True")
Real output (this machine)
Python 3.13.5
pydantic 2.13.5
pydantic-settings 2.15.0
--- loaded from .env ---
app_name PyInns Demo API
debug True bool
api_key_prefix sk-demo…
database_url postgresql://user:pass@localhost:5432/app
cors_origins ['http://localhost:3000', 'https://app.example.com']
max_retries 3
model_dump_keys ['api_key', 'app_name', 'cors_origins', 'database_url', 'debug', 'max_retries']
model_dump_debug True
--- env_prefix MYAPP_ ---
prefixed_app_name Prefixed Service
prefixed_debug False
prefixed_max_retries 5
--- validation error ---
error_type string_too_short
error_loc ('api_key',)
error_msg String should have at least 8 characters
validation_failed True
OK pydantic-settings BaseSettings + Field + env_prefix + validation
Highlights: debug is a real bool, cors_origins parses as a two-item list, env_prefix remaps MYAPP_* vars, and a short api_key fails with string_too_short before the app boots.
Common upgrades
- Secrets: keep
api_keyout of logs; dump withmodel_dump(exclude={"api_key"}). - Nested models: nest another
BaseModeland useenv_nested_delimiter="__"forDB__HOST-style keys. - FastAPI: instantiate
Settings()once at import (or via lifespan) and inject withDepends. - CI vs local: rely on process env in CI; keep
.envfor laptops only (and gitignore it).
When to use what
| Tool | Best for |
|---|---|
| pydantic-settings | Typed app config, .env, env_prefix, startup validation |
| os.environ / dotenv alone | Tiny scripts with no schema |
| Pydantic AI / BaseModel | Agent I/O and request bodies — not process env |
FAQ
Is this the same as Pydantic AI? No — Pydantic AI is for agents; pydantic-settings is for process configuration via BaseSettings.
Do I need python-dotenv? pydantic-settings loads .env for you when env_file is set (it depends on python-dotenv under the hood).
How do lists work in .env? Use a JSON array string, e.g. CORS_ORIGINS=["http://localhost:3000"].
Next steps
Replace one os.getenv cluster with a Settings class, add min_length / ge bounds on secrets and ports, then wire the same object into FastAPI dependencies or a worker entrypoint.