Skip to content

Merging#

Load configuration from multiple sources and merge them into one dataclass.

Basic Merging#

Pass multiple Source objects to dature.load():

"""Basic merging — Merge with two YAML sources."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    tags: list[str]


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
    schema=Config,
    strategy="last_wins",
)

assert config.host == "production.example.com"
assert config.port == 8080
assert config.tags == ["web", "api"]
host: "localhost"
port: 3000
tags:
  - "default"
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"

Multiple Sources#

Multiple sources use "last_wins" by default:

"""Multiple sources — implicit LAST_WINS merge."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    tags: list[str]


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
    schema=Config,
)

assert config.host == "production.example.com"
assert config.port == 8080
assert config.tags == ["web", "api"]
host: "localhost"
port: 3000
tags:
  - "default"
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"

Works as a decorator too:

"""Multiple sources as a decorator — implicit LAST_WINS merge."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"

os.environ["APP_HOST"] = "env_localhost"


@dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
    dature.EnvSource(prefix="APP_"),
)
@dataclass
class Config:
    host: str
    port: int
    debug: bool = False


config = Config()
assert config.host == "env_localhost"
assert config.port == 3000
assert config.debug is False
host: "localhost"
port: 3000
tags:
  - "default"

Merge Strategies#

Strategy Behavior
"last_wins" Last source overrides (default)
"first_wins" First source wins
"first_found" Uses the first source that loads successfully, skips broken sources automatically
"raise_on_conflict" Raises MergeConflictError if the same key appears in multiple sources with different values

Nested dicts are merged recursively. Lists and scalars are replaced entirely according to the strategy.

Last source overrides earlier ones. This is the default strategy.

"""LAST_WINS — last source overrides earlier ones."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    tags: list[str]


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
    schema=Config,
    strategy="last_wins",
)

assert config.host == "production.example.com"
assert config.port == 8080
assert config.tags == ["web", "api"]
host: "localhost"
port: 3000
tags:
  - "default"
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"

First source wins on conflict. Later sources only fill in missing keys.

"""FIRST_WINS — first source wins on conflict."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    tags: list[str]


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
    schema=Config,
    strategy="first_wins",
)

assert config.host == "localhost"
assert config.port == 3000
assert config.tags == ["default"]
host: "localhost"
port: 3000
tags:
  - "default"
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"

Uses the first source that loads successfully and ignores the rest. Broken sources (missing file, parse error) are skipped automatically — no skip_if_broken needed. Type errors (wrong type, missing field) are not skipped.

"""FIRST_FOUND — use the first source that loads successfully."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    tags: list[str]


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "nonexistent.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
    schema=Config,
    strategy="first_found",
)

# nonexistent.yaml is skipped, common_defaults.yaml is used entirely
assert config.host == "localhost"
assert config.port == 3000
assert config.tags == ["default"]
host: "localhost"
port: 3000
tags:
  - "default"

Raises MergeConflictError if the same key appears in multiple sources with different values. Works best when sources have disjoint keys.

"""RAISE_ON_CONFLICT — raises if the same key has different values."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    debug: bool


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_raise_on_conflict_a.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_raise_on_conflict_b.yaml"),
    schema=Config,
    strategy="raise_on_conflict",
)

# Disjoint keys — no conflict
assert config.host == "localhost"
assert config.port == 3000
assert config.debug is True
host: "localhost"
port: 3000
debug: true

strategy is not limited to the names above — any object implementing the SourceMergeStrategy Protocol is accepted, so you can plug in your own merge logic (e.g. let env sources override files unconditionally) while still composing the built-in strategies. See Custom Source Strategy.

For per-field strategy overrides, see Per-Field Merge Strategies. To enforce that related fields are always overridden together, see Field Groups.

Merge Parameters#

All merge-related parameters are passed directly to dature.load() as keyword arguments:

Parameter Description
strategy Global merge strategy. Default: "last_wins". See Merge Strategies
field_merges Per-field merge strategy overrides. See Per-Field Merge Strategies
field_groups Enforce related fields are overridden together. See Field Groups
skip_broken_sources Skip sources that fail to load. See Skipping Broken Sources
skip_invalid_fields Drop fields with invalid values. See Skipping Invalid Fields
expand_env_vars ENV variable expansion mode. See ENV Expansion
secret_field_names Extra secret name patterns for masking. See Masking
mask_secrets Enable/disable secret masking for all sources. See Masking
nested_resolve_strategy Default priority when both JSON and flat keys exist: "flat" (default) or "json". Applies to all sources. See Nested Resolve
nested_resolve Default per-field strategy overrides for all sources. See Nested Resolve