2025, Nov 21 01:00

Ensure .local.env Takes Precedence Over .env in FastAPI by Customizing pydantic_settings Sources

Learn why FastAPI reads .env over .local.env with pydantic_settings, and how to flip precedence via settings_customise_sources so DotEnvSettingsSource wins.

When multiple configuration files exist side by side in a FastAPI project, it is tempting to point pydantic_settings to the one you want and call it a day. Yet, even with an explicit env_file set to .local.env, the app may keep reading values from .env. The reason is not the path, nor the filename, but the precedence of settings sources inside pydantic_settings.

Reproducing the issue

Consider a project layout where three dotenv files live at the project root and the settings class sits under app/config.py:

myproject
|___ app
|____|___ config.py
|___.env
|___.local.env
|___.dev.env

A straightforward settings class might look like this:

# app/config.py

from pydantic_settings import BaseSettings, SettingsConfigDict

class AppConfig(BaseSettings):
    RUNTIME_ENV: str
    DATABASE_URL: str

    # Target the local file explicitly
    model_config = SettingsConfigDict(env_file=".local.env")

Even after trying variations such as a tuple of files, relative and absolute paths, or the legacy inner Config, the loaded values still come from .env, not .local.env.

Why .env keeps winning

pydantic_settings applies a strict priority when merging configuration sources. That order is fixed unless you change it yourself:

InitSettingsSource > EnvSettingsSource > DotEnvSettingsSource > SecretsSettingsSource

With this chain, the .env file is treated as EnvSettingsSource, while files you point to with env_file (such as .local.env) are read via DotEnvSettingsSource. Since EnvSettingsSource has higher priority than DotEnvSettingsSource, values from .env win every time.

Make .local.env take precedence

The key is to customize the source order so that DotEnvSettingsSource is consulted before EnvSettingsSource. pydantic_settings provides a hook for that. By overriding settings_customise_sources, you define the exact sequence in which sources are applied.

# 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, ...]:
        # Ensure .local.env (DotEnvSettingsSource) is applied before .env (EnvSettingsSource)
        return (
            init_settings,
            dotenv_settings,
            env_settings,
            file_secret_settings,
        )

With this order, the values parsed from .local.env will override those coming from .env. The application will still honor initialization data and file secrets, but your explicitly chosen dotenv file now rightly takes priority.

Why this detail matters

Environment management is as much about predictability as it is about convenience. When the precedence is implicit, you can end up with settings that appear to ignore your env_file configuration, leading to subtle misconfigurations across dev and local setups. Understanding and controlling the source order eliminates that ambiguity and keeps each environment reproducible.

Takeaways

If you rely on multiple dotenv files, do not assume the env_file argument alone defines precedence. pydantic_settings merges sources in a fixed order: InitSettingsSource, then EnvSettingsSource, then DotEnvSettingsSource, then SecretsSettingsSource. To ensure a specific dotenv file, such as .local.env, actually drives your configuration, override settings_customise_sources to place DotEnvSettingsSource ahead of EnvSettingsSource. This small change aligns the framework’s loading behavior with your intent and prevents .env from silently overshadowing environment-specific files on Python 3.12.8 with pydantic_settings > 2.9.1.