PydanticMiddleCoding

Как валидировать переменные окружения с помощью pydantic-settings?

pydantic-settings предоставляет BaseSettings — подкласс BaseModel, читающий переменные окружения и .env-файлы. Поля автоматически валидируются при старте приложения.

Установка и базовое использование

pydantic-settings — отдельный пакет, не входящий в pydantic по умолчанию.

# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, SecretStr
from typing import Optional

class AppSettings(BaseSettings):
    # Переменные окружения читаются автоматически (case-insensitive)
    app_name: str = "MyApp"
    debug: bool = False
    port: int = Field(default=8000, ge=1024, le=65535)

    # Чувствительные данные — SecretStr (не попадут в repr)
    secret_key: SecretStr
    database_url: str
    redis_url: str = "redis://localhost:6379"

    # Опциональные внешние сервисы
    sentry_dsn: Optional[str] = None

    model_config = SettingsConfigDict(
        env_file=".env",           # читать .env файл
        env_file_encoding="utf-8",
        case_sensitive=False,      # DATABASE_URL == database_url
        extra="ignore",            # игнорировать лишние переменные
    )

# Создаётся один раз при старте — автоматически читает окружение
settings = AppSettings()
print(settings.app_name)
print(settings.secret_key.get_secret_value())  # явное раскрытие

Файл .env и приоритет источников

# .env файл
APP_NAME=Production App
DEBUG=false
DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/mydb
SECRET_KEY=super-secret-key-here

Приоритет источников (от высшего к низшему):

  1. Аргументы конструктора: AppSettings(debug=True)
  2. Переменные окружения (environment variables)
  3. Значения из .env файла
  4. Default-значения полей

Префиксы для группировки настроек

from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseSettings(BaseSettings):
    host: str = "localhost"
    port: int = 5432
    name: str = "app"
    user: str
    password: SecretStr

    model_config = SettingsConfigDict(
        env_prefix="DB_",   # DB_HOST, DB_PORT, DB_NAME, ...
        env_file=".env",
    )

class RedisSettings(BaseSettings):
    host: str = "localhost"
    port: int = 6379
    db: int = 0

    model_config = SettingsConfigDict(
        env_prefix="REDIS_",
        env_file=".env",
    )

class Settings(BaseSettings):
    database: DatabaseSettings = DatabaseSettings()
    redis: RedisSettings = RedisSettings()
    secret_key: SecretStr

    model_config = SettingsConfigDict(env_file=".env")

settings = Settings()
print(settings.database.host)

Singleton через lru_cache (FastAPI-паттерн)

from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr, PostgresDsn

class Settings(BaseSettings):
    database_url: PostgresDsn  # встроенная валидация URL
    secret_key: SecretStr
    allowed_hosts: list[str] = ["localhost"]
    debug: bool = False

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )

@lru_cache
def get_settings() -> Settings:
    return Settings()

# В FastAPI endpoint:
from fastapi import Depends

def some_endpoint(settings: Settings = Depends(get_settings)):
    db_url = str(settings.database_url)
    return {"db": db_url}

Валидация списков и сложных типов из env

from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List

class Settings(BaseSettings):
    # ALLOWED_HOSTS=["host1","host2"] или ALLOWED_HOSTS=host1,host2
    allowed_hosts: List[str] = ["localhost"]
    # FEATURE_FLAGS={"new_ui": true, "beta": false}
    feature_flags: dict = {}

    model_config = SettingsConfigDict(
        env_file=".env",
    )

# В .env:
# ALLOWED_HOSTS=["api.example.com","www.example.com"]
# FEATURE_FLAGS={"new_ui": true}

Подводные камни

  • SecretStr в логахrepr(settings) показывает SecretStr('**********'), но settings.model_dump() возвращает сам объект SecretStr. Для сериализации в JSON используйте model_dump(mode='json') — тогда значение будет скрыто как "**********".
  • .env не читается в тестах автоматически — pytest не подгружает .env; используйте python-dotenv или monkey-patching через monkeypatch.setenv(). Лучше передавайте настройки явно: Settings(secret_key="test").
  • Приоритет env над .env — если переменная уже задана в окружении CI/CD, значение из .env-файла будет проигнорировано. Это частая причина «работает локально, не работает в CI».
  • env_prefix применяется ко всем полям — при использовании env_prefix="DB_" даже поля с именем url требуют переменную DB_URL, не URL.
  • Вложенные BaseSettings не наследуют .env родителя — каждый вложенный класс читает свой .env независимо; передайте путь явно через SettingsConfigDict(env_file=...) в каждом классе.
  • PostgresDsn хранится как объект, не strstr(settings.database_url) необходимо для передачи в SQLAlchemy create_engine().

Common mistakes

  • Описывать settings env validation только как термин и не показывать механизм на минимальном примере.
  • Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
  • Не связывать поведение с официальным контрактом Pydantic и реальной эксплуатацией.

What the interviewer is testing

  • Объясняет settings env validation через последовательность действий, а не через набор ключевых слов.
  • Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics