Nested settings models in pydantic

Flat configuration becomes unreadable once you have CACHE_HOST, CACHE_PORT, CACHE_SSL, DB_HOST, DB_PORT. Nested settings models group related fields into sub-objects while still reading from flat environment variables. This page builds them, extending Pydantic Settings Fundamentals.

Problem 1: a flat soup of prefixed fields

# ANTI-PATTERN: related fields scattered flat
class Settings(BaseSettings):
    cache_host: str
    cache_port: int
    cache_ssl: bool
    db_host: str
    db_port: int        # no grouping, no reuse

There is no structure and no way to pass “the cache config” as one object.

Problem 2: wrong delimiter, empty sub-model

# ANTI-PATTERN: single underscore collides with field names
model_config = SettingsConfigDict(env_nested_delimiter="_")   # CACHE_HOST ambiguous

A single-underscore delimiter clashes with normal field names; the sub-model ends up empty or mis-parsed.

Secure implementation

# config/nested.py
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

class CacheConfig(BaseModel):
    host: str
    port: int = 6379
    ssl: bool = True

class DBConfig(BaseModel):
    host: str
    port: int = 5432

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_nested_delimiter="__",      # double underscore avoids field-name clashes
        extra="forbid",
    )
    cache: CacheConfig
    db: DBConfig

# Env: CACHE__HOST=redis.local  CACHE__SSL=true  DB__HOST=pg.local
settings = Settings()
settings.cache.ssl   # typed bool, grouped under a reusable sub-model

env_nested_delimiter="__" maps CACHE__HOST to cache.host. The same flat variables that Kubernetes injects populate structured, reusable sub-models — exactly the format used in YAML config.

Gotchas & version-specific behaviour

  • Use __ (double underscore) as the delimiter so it never collides with field names.
  • Sub-models are plain BaseModel, not BaseSettings.
  • Defaults on sub-model fields work normally; required sub-fields raise if unset.
  • The same CACHE__HOST form works locally in a .env and in Kubernetes secrets — full parity.

Production parity checklist

  • Related fields grouped into BaseModel sub-models.
  • env_nested_delimiter="__" set; variables use the double-underscore form.
  • extra="forbid" rejects unexpected nested keys.
  • Local .env uses the same PARENT__CHILD keys as production injection.
  • Required sub-fields validated at startup.

Conclusion

Nested sub-models give structure and reuse while reading the same flat variables every platform injects. For nested file config, see Handling Nested Configuration in YAML Safely.