Skip to content

ndcorder/confmerge

Repository files navigation

confmerge

One config loader to rule them all — env vars, .env, JSON, YAML, TOML, validated with types.

Why?

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.

Installation

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]       # everything

Quick Start

from 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 values

Sources

TOMLSource

from confmerge import TOMLSource

source = TOMLSource("config.toml")
source = TOMLSource("config.toml", env="production")  # merges config.production.toml

JSONSource

from confmerge import JSONSource

source = JSONSource("config.json")
source = JSONSource("config.json", env="staging")  # merges config.staging.json

YAMLSource

from confmerge import YAMLSource  # requires: pip install confmerge[yaml]

source = YAMLSource("config.yaml")
source = YAMLSource("config.yaml", env="production")

DotEnvSource

from confmerge import DotEnvSource  # requires: pip install confmerge[dotenv]

source = DotEnvSource()              # loads .env from cwd
source = DotEnvSource("path/.env")   # explicit path

EnvSource

from 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"}}.

Merge Strategies

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)

Schema Validation

confmerge validates against dataclasses, Pydantic models, or TypedDict.

Dataclasses

from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    host: str
    port: int

config = ConfigChain(sources=sources, schema=Config).load()
# Returns a frozen dataclass instance

Pydantic Models

from pydantic import BaseModel

class Config(BaseModel):
    host: str
    port: int

config = ConfigChain(sources=sources, schema=Config).load()
# Returns a Pydantic model instance

TypedDict

from typing import TypedDict

class Config(TypedDict):
    host: str
    port: int

config = ConfigChain(sources=sources, schema=Config).load()
# Returns a validated dict

Secret Masking

Mark 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()]

Environment Profiles

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")

Frozen Config (No Schema)

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 AttributeError

CLI

confmerge 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.toml

Source formats in CLI: file paths (.toml, .json, .yaml, .yml, .env), env for all env vars, or env:PREFIX_ for prefixed env vars.

API Reference

ConfigChain(sources, schema=None, strategy=MergeStrategy.OVERRIDE)

  • 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)

Source Classes

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

Contributing

  1. Clone the repo
  2. Install dev dependencies: uv sync
  3. Run tests: uv run pytest
  4. Run linter: uv run ruff check .
  5. Format: uv run ruff format .

License

MIT — see LICENSE.

About

Unified config loader — env vars, .env, JSON, YAML, TOML, validated with types

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages