Config & Environment
Production CLIs layer configuration: defaults, config file, environment variables, and CLI flags (highest precedence). Pydantic Settings automates this pattern.
Recipe
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_url: str = "http://localhost:8000"
api_key: str = ""
debug: bool = False
model_config = {"env_prefix": "MYAPP_"}When to reach for this:
- CLI tools with multiple configuration sources
- 12-factor compliant applications
- Secrets via env vars, defaults via config file
Working Example
from pathlib import Path
from typing import Optional
import typer
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="INVOICE_",
env_file=".env",
env_file_encoding="utf-8",
)
api_url: str = "https://api.example.com"
api_key: str = ""
timeout: float = 30.0
output_dir: Path = Path("./output")
app = typer.Typer()
@app.command()
def sync(
api_url: Optional[str] = typer.Option(None, help="Override API URL"),
debug: bool = typer.Option(False, "--debug"),
):
settings = Settings()
if api_url:
settings = settings.model_copy(update={"api_url": api_url})
if debug:
typer.echo(f"Config: {settings.model_dump()}", err=True)
# use settings.api_url, settings.api_key, etc.
if __name__ == "__main__":
app()What this demonstrates:
- Pydantic Settings loads from env vars and
.envfile - CLI flags override loaded settings
env_prefixnamespaces variables (INVOICE_API_KEY)Pathtype for directory configuration
Deep Dive
Precedence (high to low)
- CLI flags
- Environment variables
.envfile- Config file (TOML/YAML)
- Code defaults
Gotchas
- Secrets in config files committed to git - leaked credentials. Fix:
.envin.gitignore; secrets only in env vars. - No validation on config values - runtime errors deep in code. Fix: Pydantic types validate at load time.
- CLI flag for every setting - unusable. Fix: flags only for common overrides; rest in config file.
- Different env var names per environment - confusion. Fix: consistent prefix; document in
--help. - Printing config with secrets - leaked in logs. Fix:
model_dump(exclude={"api_key"})for debug output.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| os.environ only | Single env var | Multiple config sources |
| configparser | INI files, no deps | Validation needed |
| dynaconf | Multi-environment | Pydantic ecosystem preferred |
FAQs
Pydantic Settings or manual env?
Pydantic Settings for typed, validated, multi-source config.
Where do secrets go?
Environment variables or secret manager. Never in config files in git.
How do I support --config?
Load TOML/YAML in a callback; merge with Settings before command runs.
.env in production?
Optional convenience. Prefer real env vars set by orchestrator.
How do I document config?
Generate from Settings fields; include in README and --help.
Config file format?
TOML preferred in Python ecosystem. YAML for complex nested config.
How do I test config?
monkeypatch.setenv or pass overrides to Settings(_env_file=None).
Multiple environments?
env_file=".env.staging" or separate config files per env.
Boolean env vars?
Pydantic parses "true", "1", "yes" as True.
How do I validate paths exist?
Field(validation_alias=...) or @field_validator on Path fields.
Related
- Typer - CLI flag overrides
- CLI Basics - env var basics
- Pydantic - validation
- CLI Best Practices - config conventions
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.