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.