2025, Nov 30 18:02
Приоритет .local.env над .env в pydantic_settings для FastAPI
Разбираем, почему .env перекрывает env_file в pydantic_settings, и покажем, как задать приоритет .local.env через settings_customise_sources в проектах FastAPI
Когда в проекте FastAPI рядом лежит несколько конфигурационных файлов, хочется просто указать pydantic_settings на нужный и на этом остановиться. Однако даже при явном env_file, заданном как .local.env, приложение может продолжать брать значения из .env. Причина не в пути и не в имени файла, а в порядке приоритета источников настроек внутри pydantic_settings.
Как воспроизвести проблему
Предположим структуру, где три dotenv‑файла лежат в корне проекта, а класс настроек находится в app/config.py:
myproject
|___ app
|____|___ config.py
|___.env
|___.local.env
|___.dev.env
Простой класс настроек может выглядеть так:
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppConfig(BaseSettings):
RUNTIME_ENV: str
DATABASE_URL: str
# Явно указываем локальный файл
model_config = SettingsConfigDict(env_file=".local.env")
Даже если перепробовать варианты — кортеж файлов, относительные и абсолютные пути или устаревший вложенный класс Config, — значения всё равно подхватываются из .env, а не из .local.env.
Почему .env постоянно «побеждает»
При объединении источников конфигурации pydantic_settings использует жёсткий приоритет. Этот порядок фиксирован, пока вы сами его не измените:
InitSettingsSource > EnvSettingsSource > DotEnvSettingsSource > SecretsSettingsSource
В этой цепочке файл .env рассматривается как EnvSettingsSource, а файлы, на которые вы указываете через env_file (например, .local.env), читаются через DotEnvSettingsSource. Поскольку EnvSettingsSource имеет более высокий приоритет, чем DotEnvSettingsSource, значения из .env неизменно берут верх.
Как отдать приоритет .local.env
Секрет — переопределить порядок источников так, чтобы DotEnvSettingsSource обрабатывался раньше EnvSettingsSource. Для этого в pydantic_settings есть специальный хук. Переопределив settings_customise_sources, вы задаёте точную последовательность применения источников.
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic_settings.sources import PydanticBaseSettingsSource
class AppConfig(BaseSettings):
RUNTIME_ENV: str
DATABASE_URL: str
model_config = SettingsConfigDict(env_file=".local.env")
@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
# Гарантируем, что .local.env (DotEnvSettingsSource) применяется раньше .env (EnvSettingsSource)
return (
init_settings,
dotenv_settings,
env_settings,
file_secret_settings,
)
С таким порядком значения из .local.env будут переопределять данные из .env. Приложение по‑прежнему учтёт параметры инициализации и secrets из файлов, но выбранный вами dotenv‑файл справедливо получит приоритет.
Почему это важно
Управление окружением — это не только удобство, но и предсказуемость. Когда порядок приоритета неочевиден, создаётся впечатление, будто env_file игнорируется, и это ведёт к незаметным расхождениям между dev‑ и локальными конфигурациями. Понимая и контролируя порядок источников, вы убираете двусмысленность и сохраняете воспроизводимость окружений.
Итоги
Если вы используете несколько dotenv‑файлов, не полагайтесь на то, что один лишь аргумент env_file задаёт приоритет. pydantic_settings объединяет источники в фиксированном порядке: сначала InitSettingsSource, затем EnvSettingsSource, потом DotEnvSettingsSource и, наконец, SecretsSettingsSource. Чтобы нужный dotenv‑файл, например .local.env, действительно управлял конфигурацией, переопределите settings_customise_sources и разместите DotEnvSettingsSource перед EnvSettingsSource. Это небольшое изменение согласует поведение загрузки фреймворка с вашим замыслом и не даст .env незаметно перекрывать файлы, специфичные для окружения, на Python 3.12.8 с pydantic_settings > 2.9.1.