One config loader to rule them all — env vars, .env, JSON, YAML, TOML, validated with types.
Python developers juggle python-dotenv for .env files, json/pyyaml/tomllib for config files, and os.environ for env vars — each with different APIs and no unified priority/merge system. confmerge loads from all common sources, merges them with explicit priority ordering, validates against a typed schema, and returns a frozen config object.
pip install confmerge # core (TOML only)
pip install confmerge[yaml] # + YAML support
pip install confmerge[dotenv] # + .env support
pip install confmerge[pydantic] # + Pydantic validation
pip install confmerge[all] # everythingfrom dataclasses import dataclass
from confmerge import ConfigChain, TOMLSource, EnvSource, DotEnvSource
@dataclass(frozen=True)
class AppConfig:
name: str
debug: bool
db_host: str
db_port: int
config = ConfigChain(
sources=[
TOMLSource("config.toml"), # lowest priority
DotEnvSource(".env"), # middle priority
EnvSource(prefix="MYAPP_"), # highest priority
],
schema=AppConfig,
).load()
print(config.name) # typed, validated, frozen
print(config.db_host) # env vars override file valuesfrom confmerge import TOMLSource
source = TOMLSource("config.toml")
source = TOMLSource("config.toml", env="production") # merges config.production.tomlfrom confmerge import JSONSource
source = JSONSource("config.json")
source = JSONSource("config.json", env="staging") # merges config.staging.jsonfrom confmerge import YAMLSource # requires: pip install confmerge[yaml]
source = YAMLSource("config.yaml")
source = YAMLSource("config.yaml", env="production")from confmerge import DotEnvSource # requires: pip install confmerge[dotenv]
source = DotEnvSource() # loads .env from cwd
source = DotEnvSource("path/.env") # explicit pathfrom confmerge import EnvSource
source = EnvSource(prefix="MYAPP_") # MYAPP_DB_HOST → db_host
source = EnvSource(prefix="MYAPP_") # MYAPP_DB__HOST → db.host (nested)The __ separator creates nested keys: MYAPP_DB__HOST=localhost becomes {"db": {"host": "localhost"}}.
from confmerge import ConfigChain, MergeStrategy
# OVERRIDE (default): later sources win
chain = ConfigChain(sources=sources, strategy=MergeStrategy.OVERRIDE)
# APPEND: lists are concatenated, scalars still override
chain = ConfigChain(sources=sources, strategy=MergeStrategy.APPEND)
# ERROR: raise on any conflicting key
chain = ConfigChain(sources=sources, strategy=MergeStrategy.ERROR)confmerge validates against dataclasses, Pydantic models, or TypedDict.
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
host: str
port: int
config = ConfigChain(sources=sources, schema=Config).load()
# Returns a frozen dataclass instancefrom pydantic import BaseModel
class Config(BaseModel):
host: str
port: int
config = ConfigChain(sources=sources, schema=Config).load()
# Returns a Pydantic model instancefrom typing import TypedDict
class Config(TypedDict):
host: str
port: int
config = ConfigChain(sources=sources, schema=Config).load()
# Returns a validated dictMark fields as secrets to mask them in logs and repr:
from dataclasses import dataclass
from typing import Annotated
from confmerge.schema import Secret
@dataclass(frozen=True)
class Config:
username: str
password: Annotated[str, Secret()]File sources support environment overlays. The base file is loaded first, then the environment-specific file is merged on top:
# Loads config.toml, then merges config.production.toml on top
source = TOMLSource("config.toml", env="production")When no schema is provided, ConfigChain.load() returns a FrozenConfig — an immutable, attribute-accessible wrapper:
config = ConfigChain(sources=sources).load()
config.db.host # attribute access
config["db"] # dict-style access
config.x = 1 # raises AttributeErrorconfmerge includes a CLI for debugging config merges:
# Dump merged config as JSON
confmerge show --sources config.toml,.env,env
# Validate against a schema
confmerge validate --schema myapp.AppConfig --sources config.tomlSource formats in CLI: file paths (.toml, .json, .yaml, .yml, .env), env for all env vars, or env:PREFIX_ for prefixed env vars.
- sources:
list[Source]— ordered list of config sources (later = higher priority) - schema: optional type (dataclass, Pydantic model, TypedDict) for validation
- strategy:
MergeStrategy.OVERRIDE | .APPEND | .ERROR
Methods:
.load()— load, merge, validate, and return config.load_dict()— load and merge, return raw dict (no validation)
All sources implement load() -> dict:
| Source | Args |
|---|---|
TOMLSource(path, env=None) |
TOML file path, optional env overlay |
JSONSource(path, env=None) |
JSON file path, optional env overlay |
YAMLSource(path, env=None) |
YAML file path, optional env overlay |
DotEnvSource(path=None) |
.env file path (default: .env in cwd) |
EnvSource(prefix="", separator="__") |
Environment variables |
- Clone the repo
- Install dev dependencies:
uv sync - Run tests:
uv run pytest - Run linter:
uv run ruff check . - Format:
uv run ruff format .
MIT — see LICENSE.