pydantic-settings

repository·main·Indexed 23 days ago

https://github.com/pydantic/pydantic-settings

A library for settings management using Pydantic that allows application configuration to be defined as type-safe Pydantic models. It provides tools for loading and validating settings from environment variables, .env files, and command-line arguments via BaseSettings and SettingsConfigDict.

Tokens
25.1K
Snippets
50
Records
81
Agent score
78%

What's inside pydantic-settings

  1. Overview of pydantic-settings

    main
    pydantic-settings is a library for settings management using Pydantic. It allows you to define your application configuration as type-safe Pydantic models, enabling easy validation and loading of settings from various sources like environment variables or configuration files.
  2. Customize settings source priority and composition

    main

    You can change the order of priority or add/remove settings sources by overriding the settings_customise_sources class method in your Settings class.

    settings_customise_sources receives four arguments representing the built-in sources: init_settings, env_settings, dotenv_settings, and file_secret_settings. It must return a tuple of callables. The order of the returned tuple determines priority: the first item has the highest priority.

    Changing Priority

    To make environment variables override __init__ arguments, return env_settings before init_settings in the tuple.

    Adding Sources

    To add a custom source, implement a class inheriting from PydanticBaseSettingsSource and include it in the returned tuple from settings_customise_sources.

    from pydantic import PostgresDsn
    from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
    
    class Settings(BaseSettings):
        database_dsn: PostgresDsn
    
        @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, ...]:
            # env_settings now has higher priority than init_settings
            return env_settings, init_settings, file_secret_settings
  3. Understand field value priority

    main

    When a setting is defined in multiple places, pydantic-settings resolves the value using the following priority (from highest to lowest):

    1. CLI Arguments: If cli_parse_args is enabled.
    2. Class Initializer: Arguments passed directly to Settings(...).
    3. Environment Variables: e.g., MY_PREFIX_FIELD.
    4. Dotenv Files: Variables loaded from a .env file.
    5. Secrets Directory: Variables loaded from the configured secrets directory.
    6. Default Values: The default values defined in the BaseSettings model fields.
  4. Use Literals and Enums as CLI choices

    main

    When a field is typed as an Enum or a Literal, the CLI parser automatically converts these into valid CLI choices, restricting the user to the defined values.

    import sys
    from enum import IntEnum
    from typing import Literal
    from pydantic_settings import BaseSettings
    
    class Fruit(IntEnum):
        pear = 0
        kiwi = 1
        lime = 2
    
    class Settings(BaseSettings, cli_parse_args=True):
        fruit: Fruit
        pet: Literal['dog', 'cat', 'bird']
    
    sys.argv = ['example.py', '--fruit', 'lime', '--pet', 'cat']
    print(Settings().model_dump())
    # {'fruit': <Fruit.lime: 2>, 'pet': 'cat'}
  5. Configure Dotenv filtering and extra field behavior

    main

    By default, if extra='forbid' is set in model_config, any entry in your .env file that does not correspond to a defined field in your BaseSettings model will raise a ValidationError.

    You can control this behavior using the dotenv_filtering setting in SettingsConfigDict:

    • 'match_prefix': Only variables matching the env_prefix will be passed to the model. This is useful for scoping a single .env file to a specific model.
    • 'only_existing': Only variables that have a corresponding field in the model will be passed. This makes the dotenv source behave like standard environment variables.

    Note on env_prefix asymmetry: If you have an env_prefix set (e.g., app_), an unprefixed entry in the .env file that matches a field name (e.g., debug=true for a field debug) is silently ignored rather than being treated as an extra field. However, a completely unrelated unprefixed entry (e.g., foo=bar) will still raise a ValidationError if extra='forbid' is active.

    from pydantic_settings import BaseSettings, SettingsConfigDict
    
    # Example of using dotenv_filtering to avoid ValidationErrors from extra keys
    class Settings(BaseSettings):
        model_config = SettingsConfigDict(
            env_file='.env',
            dotenv_filtering='only_existing'
        )
  6. Limit nesting depth with env_nested_max_split

    main

    When using env_nested_delimiter, pydantic-settings splits variables into arbitrarily deep fields by default. To prevent accidental deep nesting (e.g., when a delimiter like _ is part of a field name), use env_nested_max_split to limit the depth.

    For example, if you have a field api_key and use _ as a delimiter, API_KEY might be interpreted as api.key. Setting env_nested_max_split=1 ensures it is treated as a single field.

    class GenerationConfig(BaseSettings):
        model_config = SettingsConfigDict(
            env_nested_delimiter='_', 
            env_nested_max_split=1, 
            env_prefix='GENERATION_'
        )
        llm: LLMConfig
    
    # With export GENERATION_LLM_API_KEY='key',
    # max_split=1 ensures it maps to llm.api_key instead of llm.api.key
  7. Create mutually exclusive CLI argument groups

    main

    To ensure that only one of a set of arguments is provided, inherit from CliMutuallyExclusiveGroup.

    Constraints:

    • A CliMutuallyExclusiveGroup cannot be used within a Union type.
    • A CliMutuallyExclusiveGroup cannot contain nested models.
    from typing import Optional
    from pydantic import BaseModel
    from pydantic_settings import CliApp, CliMutuallyExclusiveGroup, SettingsError
    
    class Circle(CliMutuallyExclusiveGroup):
        radius: Optional[float] = None
        diameter: Optional[float] = None
        perimeter: Optional[float] = None
    
    class Settings(BaseModel):
        circle: Circle
    
    try:
        CliApp.run(
            Settings,
            cli_args=['--circle.radius=1', '--circle.diameter=2'],
            cli_exit_on_error=False,
        )
    except SettingsError as e:
        print(e)
        # error parsing CLI: argument --circle.diameter: not allowed with argument --circle.radius
  8. How BaseSettings works

    main
    By inheriting from BaseSettings, a model's initializer will automatically attempt to populate fields from environment variables if they are not provided as keyword arguments. If a matching environment variable is not found, the field's default value is used. This allows for type-hinted configuration classes that can be easily overridden via the environment or manually during instantiation (e.g., for testing).
  9. Disable JSON parsing for environment variables

    main

    If you want to avoid the requirement for valid JSON in environment variables, you have three options:

    1. Per-field: Annotate a field with NoDecode. This disables JSON decoding for that specific field, allowing you to use a field_validator (with mode='before') to parse the raw string (e.g., comma-separated values).
    2. Global: Set enable_decoding=False in SettingsConfigDict to disable JSON parsing for all fields.
    3. Force JSON: If global decoding is disabled, you can force JSON parsing for a specific field using the ForceDecode annotation.
    from typing import Annotated
    from pydantic import field_validator
    from pydantic_settings import BaseSettings, NoDecode
    
    class Settings(BaseSettings):
        # Disables JSON parsing; use validator to handle '1,2,3'
        numbers: Annotated[list[int], NoDecode]
    
        @field_validator('numbers', mode='before')
        @classmethod
        def decode_numbers(cls, v: str) -> list[int]:
            return [int(x) for x in v.split(',')]
  10. Use file-based settings sources (JSON, TOML, YAML)

    main

    You can load configuration from files using specialized settings sources. The available sources are:

    • JsonConfigSettingsSource: Requires json_file and json_file_encoding.
    • PyprojectTomlConfigSettingsSource: Requires pyproject_toml_depth (optional) and pyproject_toml_table_header (optional).
    • TomlConfigSettingsSource: Requires toml_file and toml_table_header (optional).
    • YamlConfigSettingsSource: Requires yaml_file and yaml_file_encoding.

    To use these, override the settings_customise_sources class method in your BaseSettings subclass and return the desired source(s).

    from pydantic_settings import (
        BaseSettings,
        PydanticBaseSettingsSource,
        SettingsConfigDict,
        TomlConfigSettingsSource,
    )
    
    class Settings(BaseSettings):
        foobar: str
        model_config = SettingsConfigDict(toml_file='config.toml')
    
        @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, ...]:
            return (TomlConfigSettingsSource(settings_cls),)
  11. Resolve GCP Project ID from other settings sources

    main

    You can avoid hardcoding the project_id by allowing it to be resolved from a previously loaded settings source (like an environment variable).

    Use the project_id_field argument in GoogleSecretManagerSettingsSource to specify which field (or alias) contains the project ID. The value must match the key used by the earlier source in its current_state (typically the field's preferred alias).

  12. Parse lists and dictionaries via CLI

    main

    Pydantic settings supports multiple styles for parsing complex types from the CLI:

    Lists support:

    • JSON style: --field='[1,2]'
    • Argparse style: --field 1 --field 2
    • Lazy style: --field=1,2

    Dictionaries support:

    • JSON style: --field='{"k1": 1, "k2": 2}'
    • Environment variable style: --field k1=1 --field k2=2

    You can mix these styles (e.g., using lazy list style and JSON dictionary style in the same command).

    import sys
    from pydantic_settings import BaseSettings
    
    class Settings(BaseSettings, cli_parse_args=True):
        my_list: list[int]
        my_dict: dict[str, int]
    
    # List examples
    sys.argv = ['example.py', '--my_list', '[1,2]']
    sys.argv = ['example.py', '--my_list', '1', '--my_list', '2']
    sys.argv = ['example.py', '--my_list', '1,2']
    
    # Dictionary examples
    sys.argv = ['example.py', '--my_dict', '{"k1":1,"k2":2}']
    sys.argv = ['example.py', '--my_dict', 'k1=1', '--my_dict', 'k2=2']