Schema Evolution & Versioning

During a rolling deployment, old and new pods run at the same time against the same injected environment. If the new release renames DB_DSN to DATABASE_URL, the old pods break the instant you switch the variable — unless the schema accepts both names. This page evolves a settings schema without a downtime window.

Safe evolution is what keeps the settings model stable as requirements change, the final discipline in the type-safe validation section.

Secure implementation

# config/versioned.py
import warnings
from pydantic import AliasChoices, Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(extra="forbid", populate_by_name=True)

    # Accept the new name and the legacy name during the migration window.
    database_url: str = Field(
        validation_alias=AliasChoices("DATABASE_URL", "DB_DSN"),
    )

    # Deprecated: still accepted, but warns until the removal release.
    legacy_timeout: int | None = None

    @model_validator(mode="after")
    def warn_deprecated(self) -> "Settings":
        if self.legacy_timeout is not None:
            warnings.warn(
                "legacy_timeout is deprecated; use REQUEST_TIMEOUT (removed in v3).",
                DeprecationWarning,
                stacklevel=2,
            )
        return self

AliasChoices lets both old and new pods validate during the overlap. The deprecation warning gives operators a removal deadline without breaking anything today.

Configuration reference

Tool Purpose Removal step
AliasChoices(new, old) Accept both names Drop old after cutover
populate_by_name=True Allow field name and alias Keep while aliasing
Optional deprecated field Soft-remove a setting Delete after warning window
model_validator warning Signal deprecation Promote to error, then remove

Deployment parity: local to production

  1. Local dev — both names work; developers are unaffected mid-migration.
  2. CI — assert the model validates with the old name and the new name during the window.
  3. Staging — switch the injected variable to the new name; old-name pods still pass.
  4. Production — roll out, confirm no deprecation warnings fire, then remove the alias in the next release.

Security boundaries & guardrails

  • Never rename and remove in the same release; always run a two-name overlap window.
  • Keep extra="forbid" — but remember a removed field then becomes an error, so coordinate removal with operators.
  • Treat default-value changes as behavioural changes and announce them.
  • Version the schema in changelogs so every environment knows which names are valid.

Troubleshooting

  • Old pods crash after a variable rename — the alias window was skipped; re-add AliasChoices and redeploy.
  • extra not permitted after removing a field — an environment still sets the old variable; stop setting it before removing the field. See Handling Breaking Changes in Production Config Schemas.
  • Deprecation warning never appearswarnings filters suppress it; run CI with PYTHONWARNINGS=error::DeprecationWarning.
  • Both names set with different valuesAliasChoices takes the first match; document which name wins.

Frequently asked questions

How do I rename a config field without breaking running pods?

Accept both names during a migration window using AliasChoices in validation_alias, deploy, switch every environment to the new name, then remove the old alias in a later release. The two-name window means old and new pods both validate.

How can I deprecate a configuration field safely?

Keep the field optional, emit a warning in a model_validator when it is supplied, and document the removal release. Remove it only after every environment has stopped setting it.

Is changing a default value a breaking change?

It can be. A changed default silently alters behaviour for any environment that relied on the old one. Treat default changes like code changes — announce them and roll them out deliberately.

Conclusion

The invariant: no field is renamed or removed without an overlap window where both the old and new schema validate. Evolution is additive first, subtractive only after every environment has migrated.