Essay

Validate settings at startup

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.