mrkeyoor.com_
Thu 06 Aug 01:03 UTC
PyPIUtilsupdated 05 Aug 2026

pydantic-settings

pydantic-settings is the configuration layer of the Pydantic family: you declare a class with typed fields and it fills them from environment variables, .env files, secrets directories, CLI arguments, or cloud secret stores, then validates everything with Pydantic before your app starts. It is the old BaseSettings from Pydantic v1, split into its own package for v2. The payoff is failing at boot with a clear error when DATABASE_URL is missing or PORT is not an int, instead of failing at 3am when the code path finally runs.

Verdict

If Pydantic is already in your stack, this is the obvious way to do configuration and there is little reason to hand-roll it. Outside the Pydantic world it is a heavy hammer for reading env vars, and dynaconf remains the better fit for elaborate multi-environment setups.

API stability4/5The 2.x line has been steady since 2023 with additive releases (CLI support, cloud secret sources); the deduction is history, since the v1-to-v2 split moved the import path and config style, and old tutorials still teach the dead form.
Docs4/5One long, example-dense page on docs.pydantic.dev covers env names, nesting, sources, CLI, and secrets honestly; because it is a single page, finding the one paragraph on source precedence or JSON parsing of complex types takes scrolling.
Maintenance5/5Pushed two days before this review, backed by the Pydantic organization with commercial funding behind it, and the tracker holds only a few dozen open issues and PRs.
Ecosystem4/5It is the assumed config layer in FastAPI tutorials and ships first-party sources for TOML, YAML, and the AWS, Azure, and Google secret managers; there is little third-party plugin scene beyond that, because the built-ins cover most of it.

Use it if

  • You are already on Pydantic v2 (FastAPI apps almost always are), so typed, validated settings cost one small extra package
  • You want config as a typed object with IDE completion instead of os.environ.get() strings scattered through the codebase
  • You layer sources: defaults in code, .env for local dev, real env vars in production, Docker secrets files in swarm/k8s, and you want documented precedence between them
  • You need nested config (db.host, redis.url) mapped from flat env vars via a delimiter, which hand-rolled os.environ parsing gets wrong fast
Skip it if

Setup reality

pip install pydantic-settings brings pydantic, pydantic-core, and python-dotenv along; .env support works out of the box. The learning curve is the sources model: init kwargs beat env vars, which beat .env values, which beat secrets files, and changing that order means overriding settings_customise_sources, a classmethod with a five-argument signature people mostly copy from the docs. Migrators from Pydantic v1 need the import moved from pydantic to pydantic_settings and Config classes rewritten as SettingsConfigDict. Extra inputs are ignored by default, but typos in env var names just silently leave defaults in place.

Patterns

Typed settings from environment variablesbasic-settings

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    port: int = 8000
    debug: bool = False

settings = Settings()
print(settings.port)

Matching is case-insensitive by default, so DATABASE_URL fills database_url. Missing required fields raise a ValidationError at construction, which is exactly the boot-time failure you want.

Read a .env file with env var overrideload-dotenv-file

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )
    api_key: str
    debug: bool = False

python-dotenv is already a dependency, so no extra install. Real environment variables always beat .env values, which is what lets production env vars override the local file.

Namespace all variables with a prefixenv-prefix

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="MYAPP_")
    host: str = "127.0.0.1"
    port: int = 8000

# reads MYAPP_HOST and MYAPP_PORT

The prefix applies to every field and is invisible in code. If a field also sets validation_alias, the alias wins and the prefix is not applied to it, a detail that surprises people.

Nested models from flat env varsnested-settings

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

class DbConfig(BaseModel):
    host: str = "localhost"
    port: int = 5432

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_nested_delimiter="__")
    db: DbConfig = DbConfig()

# DB__HOST=db.prod.internal DB__PORT=6432

Without env_nested_delimiter set, DB__HOST does nothing and the whole nested object must arrive as one JSON env var named DB. Pick the double underscore; single underscores collide with field names.

Complex types come from JSON stringslist-and-dict-fields

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    allowed_hosts: list[str] = []
    feature_flags: dict[str, bool] = {}

# works:  ALLOWED_HOSTS='["a.com", "b.com"]'
# fails:  ALLOWED_HOSTS=a.com,b.com  (json.decoder.JSONDecodeError)

This is the library's number one gotcha: non-scalar fields are parsed as JSON. If you must accept comma-separated values, take the field as str and split it in a field_validator.

Read Docker/Kubernetes secrets filesdocker-secrets-dir

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(secrets_dir="/run/secrets")
    db_password: str

# /run/secrets/db_password contains the value

One file per field, file name matching the field name, file contents as the value. Env vars still take precedence over secrets files, so a stray DB_PASSWORD in the environment wins silently.

Map a field to differently named variablesfield-alias

from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    redis_dsn: str = Field(
        validation_alias=AliasChoices("REDIS_URL", "CACHE_URL"),
    )

AliasChoices tries names in order, which is how you accept a legacy variable name during a migration. Aliases are matched case-insensitively like everything else here.

Parse argv into settingscli-arguments

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(cli_parse_args=True)
    verbose: bool = False
    workers: int = 1

settings = Settings()
# python app.py --verbose true --workers 4

Added in the 2.2+ era and now a full subcommand-capable CLI layer. Nested models become dotted flags like --db.host. CLI values sit above env vars in the default precedence.

Change which source winscustomise-source-order

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource

class Settings(BaseSettings):
    api_key: str = ""

    @classmethod
    def settings_customise_sources(
        cls, settings_cls, init_settings, env_settings,
        dotenv_settings, file_secret_settings,
    ):
        # make .env beat real env vars (default is the reverse)
        return (init_settings, dotenv_settings, env_settings, file_secret_settings)

Earlier in the returned tuple means higher priority. This same hook is where you insert custom sources such as the built-in TOML, YAML, or cloud secret manager sources.

Deterministic settings in testsoverride-in-tests

def test_handler(monkeypatch):
    monkeypatch.setenv("DATABASE_URL", "sqlite://")
    settings = Settings(_env_file=None, debug=True)
    assert settings.debug is True

Init kwargs outrank every other source, so passing values directly is the cleanest override. _env_file=None stops a developer's local .env from leaking into CI test runs.

Alternatives

PackageRegistryPick it when
python-dotenvPyPIYou just need .env files loaded into os.environ with zero opinions and zero heavy dependencies.
dynaconfPyPIMulti-environment layered config with toml/yaml files, merging, and Vault/Redis backends is the actual requirement.
environsPyPIYou want typed env parsing with validation but prefer a small marshmallow-based library over the Pydantic stack.
python-decouplePyPIDjango-style projects that want strict separation of settings from code with a tiny, stable API.