Skip to content

mini.config

Keep configuration logic in regular Python modules. Start with a single dictionary, then scale into parameterized templates, inheritance chains, and CLI-friendly overrides without learning a new DSL.

Everything you write stays Python: functions, conditionals, list comprehensions, imports. mini.config focuses on loading, layering, and mutating dictionaries so you can drop the result into any workflow.

Highlights

  • Load any Python config module or callable with load.
  • Compose configs via parents = [...] chains or by supplying multiple paths at once.
  • Parameterize configs with function arguments; inject runtime values with params.
  • Adjust values on the fly using apply_overrides and a compact CLI syntax.
  • Control merge behavior with Delete() and Replace(value).
  • Snapshot final dictionaries back to Python with dump, or pretty-print them with format.

Quick tour

  1. Define a base config as a plain dictionary:

    # configs/base.py
    config = {
        "model": {
            "encoder": {"channels": 64},
            "head": {"in_channels": 64, "out_channels": 10},
        },
        "optimizer": {"type": "adam", "lr": 3e-4},
        "trainer": {"max_steps": 50_000},
    }
    
  2. Extend it with parents and parameters when the project grows:

    # configs/finetune.py
    parents = ["base.py"]
    
    def config(num_classes=10, max_steps=10_000, warmup_steps=1_000):
        return {
            "model": {
                "head": {"out_channels": num_classes},
            },
            "scheduler": {
                "type": "linear_warmup_cosine_decay",
                "warmup_steps": warmup_steps,
                "decay_steps": max_steps - warmup_steps,
            },
            "trainer": {"max_steps": max_steps},
        }
    
  3. Load everything and apply runtime tweaks:

    from mini.config import apply_overrides, load
    
    cfg = load("configs/finetune.py", params={"num_classes": 5})
    cfg = apply_overrides(cfg, ["optimizer.lr=1e-3"])
    
  4. Feed the dictionary wherever you need it—serialize to JSON, log to disk, or hand it to any factory you already use:

    import json
    
    with open("runs/finetune_config.json", "w") as f:
        json.dump(cfg, f, indent=2)
    

Anatomy of a config module

Each config file is just Python. The loader only pays attention to two attributes:

  • config: dictionary or callable returning a dictionary.
  • parents: string or list of strings pointing to other config files (paths resolved relative to the current file).

Use a dictionary (config = {...}) when the configuration is static. Use a callable when you want parameters; default argument values become part of the config defaults automatically.

def config(batch_size: int = 64, *, device: str = "cuda"):
    return {
        "data": {"batch_size": batch_size},
        "trainer": {"device": device},
    }

Multiple parents or paths

Parents are loaded depth-first (left to right), so later parents override earlier ones before the child applies its updates.

# configs/experiment.py
parents = ["base.py", "schedules/cosine.py"]

config = {"trainer": {"max_epochs": 40}}

load also accepts a sequence of paths when you want to compose parts dynamically:

cfg = load(
    [
        "configs/base.py",
        "configs/backbones/resnet.py",
        "configs/modes/eval.py",
    ]
)

Runtime overrides

apply_overrides(config_dict, sequence_of_strings) mutates the dictionary in place. Each string uses a compact syntax designed for CLI usage.

  • path=value → assign (dict keys or list indices)
  • path+=value → append to a list
  • path-=value → remove a matching element from a list
  • path!= → delete a key or remove a list index

Values are parsed with ast.literal_eval, so strings, numbers, booleans, lists, dictionaries, and None all work (as long as they do not contain whitespace). If parsing fails, the raw string is used, so quoting strings is usually not necessary.

apply_overrides(
    cfg,
    [
        "optimizer.lr=5e-4",
        "trainer.max_steps=10_000",
        "trainer.hooks+='wandb'",
        "trainer.hooks-='checkpoint'",
        "data.pipeline[0]!=",
    ],
)

Merge semantics

When configs are layered, mini.config walks the override dictionary and combines it with the base using:

  • Dicts merge recursively.
  • Delete() removes the key entirely.
  • Replace(value) uses value as-is without deeper merging.
  • Otherwise the override value replaces the base.
from mini.config import Delete, Replace, merge

base = {
    "optimizer": {
        "lr": 3e-4,
        "weight_decay": 0.01,
        "schedule": {"type": "linear", "warmup": 1_000},
    },
    "trainer": {"hooks": ["progress", "checkpoint"]},
}

override = {
    "optimizer": {
        "weight_decay": Delete(),
        "schedule": Replace({"type": "cosine", "t_max": 20_000}),
    },
    "trainer": {"steps": 10_000, "hooks": ["progress"]},
}

merged = merge(base, override)

merge is exported in case you want to reuse the algorithm, but load and apply_overrides already rely on it internally.

Formatting and snapshots

Freeze the exact configuration you ran:

from pathlib import Path
from mini.config import dump, format

print(format(cfg))  # Black-formatted string
dump(cfg, Path("runs/2024-01-10/config_snapshot.py"))
  • format returns a nicely formatted string—useful for logging.
  • dump writes the same representation to disk with a short header and # fmt: off. Because the file is valid Python, you can load it again with load.

Tips for structuring configs

  • Organize by concern: configs/data/imagenet.py, configs/optim/adamw.py, configs/modes/eval.py.
  • Expose helper functions alongside config for reusable snippets.
  • Pair with object builders only if you want to: the output is a plain dict, so you can plug it into your own factories or formats.
  • Prefer parameters over repeated overrides when you always tweak the same value.

API reference

CLASS DESCRIPTION
Delete

Sentinel that removes a key from a merged config.

Replace

Sentinel that forces a value to replace a mapping during merge.

FUNCTION DESCRIPTION
load

Load config modules from one or more paths, apply params, and merge the results.

apply_overrides

Apply CLI-style override strings to a config dictionary.

merge

Recursively merge two dictionaries, honoring Delete/Replace sentinels.

dump

Persist a config dictionary to a ruff-formatted Python file.

format

Return a ruff-formatted string representation of the config dictionary.

Delete

Sentinel that removes a key from a merged config.

Replace

Sentinel that forces a value to replace a mapping during merge.

load

load(path: PathLike | Sequence[PathLike], params: dict | None = None)

Load config modules from one or more paths, apply params, and merge the results.

Parent configs (via parents) are resolved first, then later paths override earlier ones. Callable configs receive params (defaults come from signatures); plain dict configs are merged directly.

apply_overrides

apply_overrides(cfg: dict, overrides: Sequence[str])

Apply CLI-style override strings to a config dictionary.

Supports assignment (=), append (+=), delete (!=), and removal from list (-=) using dotted/indexed key paths like model.layers[0].units. Returns a shallow copy with overrides applied.

merge

merge(base: dict, override: dict)

Recursively merge two dictionaries, honoring Delete/Replace sentinels.

If both sides contain dicts, merge continues down the tree. Delete removes a key from the base config, Replace overwrites without further deep merging, and other values simply override. Returns a new dictionary without mutating the inputs.

dump

dump(config: dict, path: PathLike)

Persist a config dictionary to a ruff-formatted Python file.

format

format(config: dict) -> str

Return a ruff-formatted string representation of the config dictionary.