C.W.K.
Stream
Lesson 07 of 08 · published

Application Config — pydantic, dataclass, env override

~10 min · yaml, config, pydantic, dataclass

Level 0Plaintext
0 XP0/64 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

YAML in your app, env vars on top

Modern services typically layer config: a YAML file holds defaults, environment variables override per-deployment, runtime args override per-invocation. The pattern: parse the YAML into a strongly-typed config object, then let env vars patch fields by name.

Pydantic Settings (Python)

Pydantic V2 ships pydantic_settings: define a BaseSettings subclass, point it at a YAML file (via a custom source), and env vars auto-override fields. Type errors at parse time catch typos before deploy.

The 'one type per setting' principle

Define types up front: port: int = 8000, log_level: Literal["debug","info","warn","error"] = "info". The parser will reject values that don't fit. This is the same idea as JSON Schema, in your language's type system.

Principle: never read raw YAML into a dict and reach into it with string keys (config["server"]["port"]). Wrong key, missing key, wrong type — all silent runtime crashes. A typed config object turns those into parse-time errors.

Code

Pydantic Settings + YAML·python
from pathlib import Path
from typing import Literal
import yaml
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource

class YamlConfigSource(PydanticBaseSettingsSource):
    def get_field_value(self, *args, **kwargs):
        return None  # delegate to __call__
    def __call__(self):
        path = Path('config.yaml')
        if path.exists():
            return yaml.safe_load(path.read_text()) or {}
        return {}

class Settings(BaseSettings):
    port: int = 8000
    host: str = 'localhost'
    log_level: Literal['debug', 'info', 'warn', 'error'] = 'info'
    database_url: str

    @classmethod
    def settings_customise_sources(cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings):
        return (init_settings, env_settings, YamlConfigSource(settings_cls), dotenv_settings, file_secret_settings)

settings = Settings()
print(settings.port)  # 8000 from YAML, or override via PORT env var
config.yaml·yaml
port: 8000
host: 0.0.0.0
log_level: info
database_url: postgresql://postgres:dev@localhost/pippa
Environment override at runtime·bash
# Override one field — env vars beat YAML
LOG_LEVEL=debug PORT=9000 python -m myapp

# In a Dockerfile / k8s ConfigMap
env:
  - name: LOG_LEVEL
    value: debug
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: pippa-secrets
        key: database-url

External links

Exercise

Take a small app you've written that reads config. If it currently does yaml.safe_load + dict access, port it to a typed config object (pydantic, attrs, dataclass + cattrs). Try to read a missing or wrong-type field and watch the parse fail at boot — that's the win.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.