A missing database URL should not surface as a connection error twenty minutes into a deploy. A typo in a port number should not become a timeout in production. Configuration is input — often from the environment, a secret store, or a YAML file someone edited by hand. Treat it like a trust boundary.
The cheapest place to enforce that boundary is startup. If the process cannot prove its settings are valid, it should refuse to run.
os.getenv is not a schema
Reading strings from the environment is fine for a prototype. In a service, every
os.getenv("PORT") or "8000" spreads defaults, types, and requiredness across
the codebase. One call site coerces to int; another forgets. One path assumes
a URL exists; another only fails when the client is used.
That is the same failure mode as unvalidated JSON — delayed, scattered, and hard to explain in an incident review.
One settings model, load once
Declare the shape of your configuration the same way you declare a request body. With Pydantic settings, that looks like a single model loaded when the app boots:
from pydantic import HttpUrl, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
app_name: str = "api"
debug: bool = False
database_url: SecretStr
redis_url: HttpUrl
workers: int = 2
settings = Settings() # raises at import / lifespan if invalid
Missing DATABASE_URL, a non-integer WORKERS, or a garbage Redis
URL fails immediately with a structured error. You fix the deploy config before traffic
arrives — not after the first request that happens to touch Redis.
Fail fast beats fail deep
Startup validation is proactive. It turns “we hope staging env matches prod” into a checklist the process itself enforces. Health checks and readiness probes become honest: if the container is up, the settings already parsed.
Keep secrets as SecretStr (or equivalent) so logs and traces do not print
credentials by accident. Keep optional flags optional and required connections required —
do not paper over a missing broker with a silent default that disables a critical path.
Where this fits
APIs, workers, and CLIs all benefit. Load settings once at the edge of the process, pass
typed objects inward, and stop sprinkling getenv through business logic. The
rule is the same as for request bodies: validate when untrusted data enters, then trust
the typed result.
Bad config is bad data. Catch it at boot.