I am building some configuration logic for a Python 3 app, and trying to use pydantic
and pydantic-settings
to manage validation etc. I'm able to load raw settings from a YAML file and create my settings object from them. I'm also able to read a value from an environment variable. But I can't figure out how to make the environment variable value take precedence over the raw settings:
import os
import yaml as pyyaml
from pydantic_settings import BaseSettings, SettingsConfigDict
class FooSettings(BaseSettings):
foo: int
bar: str
model_config = SettingsConfigDict(env_prefix='FOOCFG__')
raw_yaml = """
foo: 13
bar: baz
"""
os.environ.setdefault("FOOCFG__FOO", "42")
raw_settings = pyyaml.safe_load(raw_yaml)
settings = FooSettings(**raw_settings)
assert settings.foo == 42
If I comment out foo: 13
in the input yaml, the assertion passes. How can I make the env value take precedence?
Are you sure you want the environment to take precedence? While not ubiquitous, it is very common for environment variables to have the lowest precedence (typically, the ordering is built-in defaults, then environment variables, then configuration files, then command line options). Deviating from this convention can be surprising.
You could get the behavior you want for a specific field by adding a field validator that checks for the appropriate environment variable and uses that value in preference to an existing value if it is avaiable. Something like:
import os
import yaml as pyyaml
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import field_validator
class FooSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="FOOCFG__")
foo: int
bar: str
@field_validator("foo", mode="after")
@classmethod
def validate_foo(cls, val):
'''Always use the value from the environment if it's available.'''
if env_val := os.environ.get(f"{cls.model_config['env_prefix']}FOO"):
return int(env_val)
return val
raw_yaml = """
foo: 13
bar: baz
"""
os.environ.setdefault("FOOCFG__FOO", "42")
raw_settings = pyyaml.safe_load(raw_yaml)
settings = FooSettings(**raw_settings)
assert settings.foo == 42
If you wanted to do this for all fields, you could use a model validator instead. Maybe something like this?
@model_validator(mode="after")
@classmethod
def validate_foo(cls, data):
for field in cls.model_fields:
env_name = f'{cls.model_config["env_prefix"]}{field.upper()}'
if env_val := os.environ.get(env_name):
setattr(data, field, type(getattr(data, field))(env_val))
return data