Конфигурация: 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=...).