Twelve-factor apps read config from the environment, and validating it at startup prevents late surprises.
Configuration lives in the separate package pydantic-settings (pip install pydantic-settings). Subclass BaseSettings instead of BaseModel and each field is filled, in priority order, from:
1. arguments passed to the constructor 2. environment variables (case-insensitive by default) 3. a dotenv file, if env_file is configured 4. secrets files in secrets_dir 5. the field's default
Environment values are strings, and Pydantic converts them using the field type: "true" becomes a bool, "8000" an int, and complex types such as list[str] or nested models are parsed from JSON strings.
Configure with model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env", env_nested_delimiter="__"). With a prefix, field port is read from APP_PORT. The nested delimiter lets APP_DB__HOST fill db.host.
Use SecretStr for passwords and tokens: the value is masked as ********** in repr and logs, and you read it explicitly with .get_secret_value().
The usual pattern is to create one settings object at startup, so a missing or malformed variable crashes immediately with a clear error instead of failing on the first request. In web apps, wrap construction in a function decorated with functools.lru_cache so it is built once and easy to override in tests.
Example
import os
from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class DB(BaseModel):
host: str = "localhost"
port: int = 5432
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="APP_", env_nested_delimiter="__")
debug: bool = False
allowed_hosts: list[str] = []
api_key: SecretStr
db: DB = DB()
os.environ.update({"APP_DEBUG": "true", "APP_API_KEY": "s3cr3t",
"APP_ALLOWED_HOSTS": '["a.com","b.com"]', "APP_DB__PORT": "6543"})
s = Settings()
print(s.debug, s.allowed_hosts, s.db.port) # True ['a.com', 'b.com'] 6543
print(s.api_key) # **********
print(s.api_key.get_secret_value()) # s3cr3tRemember
- BaseSettings reads fields from env vars, .env files and secrets
- env_prefix and env_nested_delimiter map variable names to fields
- SecretStr keeps secrets out of logs and reprs
Watch out for
- Installing only pydantic: BaseSettings moved to the separate pydantic-settings package in v2.