Environment variables do not override values set in the YAML configuration file. This inverts levels 2 and 3 of the documented configuration priority, and it fails silently — no warning, no error.
Found while writing the configuration recipe for #15.
Behavior
The rule is per-key: a key absent from the YAML file is overridable by its MLDP_* env var; a key present in the file is not.
Given a mldp-config.yaml containing only ingestion: {host: localhost}:
| Env var |
Present in YAML? |
Resolves to |
Correct? |
MLDP_INGESTION_HOST=prod.example.com |
yes |
localhost |
❌ env ignored |
MLDP_INGESTION_PORT=443 |
no |
443 |
✅ |
Reproduction
mldp-config.yaml:
ingestion:
host: localhost
import os
from dp_python_lib.config import MldpConfig
cfg = MldpConfig.from_yaml("mldp-config.yaml")
print(cfg.ingestion.host) # 'localhost' <- MLDP_INGESTION_HOST ignored
print(cfg.ingestion.port) # 443 <- MLDP_INGESTION_PORT applied
MLDP_INGESTION_HOST=prod.example.com MLDP_INGESTION_PORT=443 python repro.py
Reproduced on dp-python-lib 1.15.0 with pydantic-settings 2.14.2.
Cause
MldpConfig.from_yaml() (src/dp_python_lib/config/config.py:82-109) flattens the YAML into flat_data and passes it as constructor keyword arguments:
return cls(**flat_data) # config.py:109
In pydantic-settings, init kwargs are the highest-priority source — above environment variables. So every key the YAML file happens to define becomes un-overridable.
This is on the path MldpClient() actually takes: load_config() (loader.py:105) calls from_yaml() whenever a config file is discovered, and mldp-config.yaml in the working directory is discovered automatically.
Why it wasn't caught
tests/unit/test_config.py::test_environment_variable_override (line 87) constructs MldpConfig() directly, where env vars are the highest-priority source. It passes, and correctly so.
No test covers from_yaml() or load_config() with a YAML file present — which is the real-world path. The bug sits exactly in that gap.
Impact
Breaks the deployment pattern env vars exist for: one built image, per-environment overrides. A deployment that ships a mldp-config.yaml and expects MLDP_* to retarget it will silently connect to the wrong host.
The silence is the worst part — the client connects successfully, just not where intended.
Suggested fix
Route YAML through a settings source ranked below env vars, rather than through init kwargs, via settings_customise_sources:
@classmethod
def settings_customise_sources(cls, settings_cls, init_settings, env_settings,
dotenv_settings, file_secret_settings):
# documented priority: init > env > yaml > defaults
return (init_settings, env_settings, YamlSource(settings_cls), file_secret_settings)
I verified this approach restores the documented order on the current pydantic-settings version:
- key in YAML + env var set → env wins ✅
- key in YAML, no env var → YAML wins (over the built-in default) ✅
- key absent from YAML → default, or env var if set ✅
Whatever the fix, it should come with tests covering load_config() with a YAML file present, since that is the untested path where this hid.
Docs
Documented as a known bug with a ⚠️ callout in doc/cookbook/connecting.md (#15), including the verified workaround: keep the setting out of the YAML, or repoint the whole file with MLDP_CONFIG_FILE (that variable is read before the file is loaded, so it behaves as documented).
Remove that callout when this is fixed. README.md and CLAUDE.md both currently document the intended priority as though it works.
Environment variables do not override values set in the YAML configuration file. This inverts levels 2 and 3 of the documented configuration priority, and it fails silently — no warning, no error.
Found while writing the configuration recipe for #15.
Behavior
The rule is per-key: a key absent from the YAML file is overridable by its
MLDP_*env var; a key present in the file is not.Given a
mldp-config.yamlcontaining onlyingestion: {host: localhost}:MLDP_INGESTION_HOST=prod.example.comlocalhostMLDP_INGESTION_PORT=443443Reproduction
mldp-config.yaml:Reproduced on dp-python-lib 1.15.0 with pydantic-settings 2.14.2.
Cause
MldpConfig.from_yaml()(src/dp_python_lib/config/config.py:82-109) flattens the YAML intoflat_dataand passes it as constructor keyword arguments:In pydantic-settings, init kwargs are the highest-priority source — above environment variables. So every key the YAML file happens to define becomes un-overridable.
This is on the path
MldpClient()actually takes:load_config()(loader.py:105) callsfrom_yaml()whenever a config file is discovered, andmldp-config.yamlin the working directory is discovered automatically.Why it wasn't caught
tests/unit/test_config.py::test_environment_variable_override(line 87) constructsMldpConfig()directly, where env vars are the highest-priority source. It passes, and correctly so.No test covers
from_yaml()orload_config()with a YAML file present — which is the real-world path. The bug sits exactly in that gap.Impact
Breaks the deployment pattern env vars exist for: one built image, per-environment overrides. A deployment that ships a
mldp-config.yamland expectsMLDP_*to retarget it will silently connect to the wrong host.The silence is the worst part — the client connects successfully, just not where intended.
Suggested fix
Route YAML through a settings source ranked below env vars, rather than through init kwargs, via
settings_customise_sources:I verified this approach restores the documented order on the current pydantic-settings version:
Whatever the fix, it should come with tests covering
load_config()with a YAML file present, since that is the untested path where this hid.Docs
Documented as a known bug with a⚠️ callout in
doc/cookbook/connecting.md(#15), including the verified workaround: keep the setting out of the YAML, or repoint the whole file withMLDP_CONFIG_FILE(that variable is read before the file is loaded, so it behaves as documented).Remove that callout when this is fixed.
README.mdandCLAUDE.mdboth currently document the intended priority as though it works.