Конфигурация: BaseSettings и model_config

Настройки приложения (URL базы, секреты, флаги) не зашивают в код — их читают из окружения. BaseSettings из pydantic-settings делает это типобезопасно, а model_config настраивает поведение модели.

BaseSettings — это базовый класс из пакета pydantic-settings, который читает значения полей не из переданного словаря, а из переменных окружения и файла .env, валидируя их теми же типами Pydantic. По сути — типизированная замена «доставать всё из os.environ вручную».

Любому реальному приложению нужна конфигурация: адрес базы данных, секретный ключ, режим отладки, лимиты. Зашивать их в код нельзя (секреты утекут в репозиторий, а значения будут разными на ноутбуке, в тестах и на проде). Двенадцатифакторный подход говорит: конфигурацию держим в окружении. Но os.environ["PORT"] отдаёт строку без проверок — легко получить None или нечисло и упасть глубоко в рантайме.

Этот урок — про BaseSettings (чтение настроек из env и .env с валидацией), про model_config как единый центр настройки поведения модели, а также про строгий режим (strict) и неизменяемые (frozen) модели, полезные именно для конфигурации.

Зачем это на практике

  • Один источник правды: все настройки собраны в одном классе Settings с типами и значениями по умолчанию — видно, что вообще конфигурируется.
  • Безопасные секреты: ключи и пароли лежат в окружении/.env (который в .gitignore), а не в коде.
  • Падать рано и понятно: если обязательная переменная не задана или PORT не число — приложение не стартует с внятной ошибкой, а не ломается через час в проде.
  • Разные среды: локально читается .env, в контейнере — переменные окружения; код один и тот же.

Код использует from pydantic_settings import ... / from pydantic import ... — это серверные библиотеки, в браузере их нет, поэтому кнопки «Запустить» под такими блоками не будет: читайте их как образец.

BaseSettings: чтение из окружения и .env

Объявляете класс настроек, наследуя BaseSettings, и описываете поля как обычно. При создании Settings() значения подтягиваются из переменных окружения по именам полей (регистр имени по умолчанию не важен), с приведением к указанным типам.

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "MyApp"
    debug: bool = False
    port: int = 8000
    database_url: str          # без значения по умолчанию = обязательно

settings = Settings()
print(settings.port, type(settings.port).__name__)

Если в окружении есть PORT=9000 и DEBUG=true, то settings.port станет числом 9000, а settings.debugбулевым True (Pydantic понимает true/false/1/0/yes/no). Поле database_url без значения по умолчанию — обязательное: если переменной DATABASE_URL нет, Settings() бросит ошибку валидации, и приложение честно не запустится. Это и есть «падать рано».

Чтобы читать и файл .env, настраивают модель через model_config и SettingsConfigDict:

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        env_prefix="APP_",      # читать APP_PORT, APP_DEBUG и т.д.
    )
    port: int = 8000
    debug: bool = False

# .env:
#   APP_PORT=9000
#   APP_DEBUG=true
settings = Settings()

Здесь env_file=".env" подключает файл, а env_prefix="APP_" требует префикс — удобно, чтобы не пересекаться с чужими переменными окружения. Приоритет источников такой: явно переданный аргумент > переменная окружения > значение из .env > значение по умолчанию. То есть реальная переменная окружения перекроет то, что записано в .env.

model_config: центр настройки поведения

В Pydantic v2 поведение модели настраивается через атрибут model_config (словарь ConfigDict), а не через старый вложенный class Config из v1. Это касается любой модели, не только настроек. Самые ходовые ключи:

КлючЧто включает
frozen=Trueмодель неизменяема: поля нельзя менять после создания
strict=Trueстрогий режим: запрет «вольных» приведений типов
extra="forbid"запретить лишние поля во входных данных (ошибка вместо игнора)
str_strip_whitespace=Trueавтоматически обрезать пробелы у строковых полей
from pydantic import BaseModel, ConfigDict

class StrictUser(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
    name: str

StrictUser(name="  Анна  ")               # name == "Анна" (пробелы срезаны)
StrictUser(name="Анна", role="admin")     # ошибка: поле role лишнее

Ключ extra="forbid" особенно полезен в API: опечатка в имени поля запроса (usrname вместо username) не «проглотится» молча, а вызовет понятную ошибку.

Строгий режим: strict=True

По умолчанию Pydantic «доброжелателен»: строку "5" он охотно приведёт к числу 5. Для входных API-данных это удобно, но для конфигурации иногда хочется жёсткости — чтобы число было числом, а не строкой, иначе это сигнал ошибки в окружении. Включает это strict=True:

from pydantic import BaseModel, ConfigDict

class Lax(BaseModel):
    n: int

class Strict(BaseModel):
    model_config = ConfigDict(strict=True)
    n: int

print(Lax(n="5").n)    # 5 — строка приведена к числу
Strict(n="5")          # ошибка: ожидался int, пришла str

В нестрогом режиме "5" стало числом; в строгом — это ошибка, потому что тип не совпал буквально. Строгость можно включать и точечно на отдельное поле (через Field(strict=True) или тип StrictInt), не делая строгой всю модель. Для настроек это помогает ловить опечатки в типах ещё на старте.

Frozen-модели: неизменяемая конфигурация

Конфигурацию обычно читают один раз при старте и дальше только используют. Чтобы случайно не изменить её в рантайме, модель делают неизменяемой через frozen=True. Тогда попытка присвоить полю новое значение вызовет ошибку.

from pydantic import BaseModel, ConfigDict

class Config(BaseModel):
    model_config = ConfigDict(frozen=True)
    retries: int = 3

cfg = Config()
print(cfg.retries)   # 3
cfg.retries = 5      # ошибка: модель frozen, поле менять нельзя

У frozen=True есть приятный бонус: такая модель становится хешируемой, и её можно класть в set или использовать как ключ словаря. Для настроек неизменяемость — это страховка: один объект Settings создаётся при старте, передаётся по приложению, и никакой случайный код его не «подкрутит».

Как это работает под капотом

BaseSettings устроен как обычная модель Pydantic, но с дополнительным шагом: перед валидацией он собирает значения из источников (аргументы конструктора, переменные окружения, файл .env, при желании секреты из файлов) в один словарь — с понятным приоритетом, — и уже его прогоняет через стандартный конвейер валидации. Поэтому всё, что вы знаете про типы, валидаторы и model_config, работает и для настроек.

Флаги strict и frozen влияют на ту самую скомпилированную core schema. strict убирает из схемы шаги «мягкого» приведения типов, оставляя строгую проверку соответствия. frozen добавляет в модель запрет на присваивание после создания и автоматически делает её хешируемой. Поскольку всё это часть схемы, а не проверки в рантайме «вручную», накладные расходы минимальны, а поведение предсказуемо.

Частые ошибки

  • Импорт не из того пакета. В Pydantic v2 BaseSettings переехал в отдельный пакет pydantic-settings (from pydantic_settings import BaseSettings), а не from pydantic import BaseSettings, как было в v1.
  • Используют class Config вместо model_config. В v2 настройка идёт через атрибут model_config = ConfigDict(...) (для настроек — SettingsConfigDict). Старый вложенный class Config — это v1.
  • Коммитят .env. Файл с секретами должен быть в .gitignore; в репозиторий кладут только пример .env.example без реальных значений.
  • Меняют frozen-модель. Присваивание полю у модели с frozen=True бросает ошибку. Нужна «изменённая копия» — используйте model_copy(update={...}).
  • Ждут приведения в strict-режиме. При strict=True строка "5" не станет числом — придёт ошибка. Либо не включайте strict для таких полей, либо приводите значение заранее.

Итоги

  • BaseSettings из pydantic-settings читает поля из переменных окружения и .env с валидацией типами; поле без значения по умолчанию становится обязательным.
  • Источники имеют приоритет: аргумент > переменная окружения > .env > значение по умолчанию; env_prefix и env_file задаются в SettingsConfigDict.
  • Поведение модели в v2 настраивается атрибутом model_config (ConfigDict): extra="forbid", str_strip_whitespace и другие ключи — вместо старого class Config.
  • strict=True отключает «вольные» приведения типов (строка "5" уже не станет числом) — полезно ловить опечатки в конфиге на старте.
  • frozen=True делает модель неизменяемой и хешируемой — надёжная страховка для объекта настроек; изменённую версию получают через model_copy(update=...).
Проверьте себя
1. Откуда BaseSettings из pydantic-settings берёт значения полей?
AИз переменных окружения и файла .env, приводя их к типам полей
BТолько из словаря, переданного в конструктор
CИз базы данных приложения
DИз случайных значений по умолчанию
2. Как в Pydantic v2 правильно настроить поведение модели (например, запретить лишние поля)?
AЧерез атрибут model_config = ConfigDict(extra="forbid")
BЧерез вложенный class Config: extra = forbid
CЧерез декоратор @config над классом
DНикак — поведение модели в v2 фиксировано
3. Что произойдёт при strict=True, если в поле n: int передать строку "5"?
AБудет ошибка валидации: в строгом режиме строка не приводится к числу автоматически
BВернётся число 5, как и без strict
CВернётся строка "5" без изменений
DПоле станет None