Settings from the environment

Pydantic · Practical · 10 min

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())          # s3cr3t

Remember

  • 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.

Check yourself

  1. With env_prefix="APP_", which variable fills the field timeout?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.pydantic.dev/latest/concepts/pydantic_settings/

Builds on

Unlocks

Nothing builds on this yet.