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