Migrating from legacy config parsers to pydantic-settings v2
Most Python services start with configparser or a pile of os.environ.get calls and only later wish every value were typed and validated. Moving to pydantic-settings v2 is mechanical if you do it incrementally. This page is the migration path, extending Pydantic Settings Fundamentals.
Problem 1: scattered, untyped reads
# ANTI-PATTERN: config spread across modules, all strings, no validation
import os
DEBUG = os.environ.get("DEBUG") # the string "False" — truthy!
PORT = int(os.environ.get("PORT", "8080")) # ValueError surfaces wherever this runs
There is no single schema and no validation; every module re-parses and re-mistakes the same values.
Problem 2: pydantic v1 idioms that broke in v2
# ANTI-PATTERN: v1 style that warns or fails under pydantic v2
from pydantic import BaseSettings # moved to pydantic_settings in v2
class Config: # inner Config class -> SettingsConfigDict
env_file = ".env"
@validator("port") # -> @field_validator
def v(cls, x): ...
BaseSettings moved packages, the inner Config class became SettingsConfigDict, and @validator became @field_validator.
Secure implementation
# config/settings.py — the v2 target
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict( # replaces inner class Config
env_file=".env", extra="forbid",
)
debug: bool = False # "False"/"0"/"no" correctly parsed
port: int = 8080
@field_validator("port") # replaces @validator
@classmethod
def valid_port(cls, v: int) -> int:
if not 1 <= v <= 65535:
raise ValueError("port out of range")
return v
settings = Settings() # one object; validates at startup
Run it alongside the old code first: build Settings() at startup and assert it matches the legacy values, then delete the old reads module by module.
Gotchas & version-specific behaviour
- Import
BaseSettingsfrompydantic_settings, notpydantic, in v2. - Replace inner
class Configwithmodel_config = SettingsConfigDict(...). @validator→@field_validator(+@classmethod);@root_validator→@model_validator..dict()→.model_dump();.parse_obj()→.model_validate().- v2 keeps the lenient env-string coercion, so booleans like
"false"parse correctly — unlikebool(os.environ[...]).
Production parity checklist
- One
Settingsobject replaces all scattered reads. extra="forbid"is set so typos fail loudly.- A transition test asserts new values equal the legacy ones before old code is removed.
- CI pins
pydanticandpydantic-settingsv2 and runs with warnings as errors. - Secrets are moved to
SecretStrfields during the migration.
Conclusion
Stand up the v2 Settings object beside the legacy parser, verify parity, then remove the old reads incrementally. For the v1-to-v2 API specifics in depth, see pydantic v1 to v2 settings migration.