cfgx¶
Keep configuration logic in regular Python modules. Start with a single dictionary, then scale into lazy computed values, inheritance chains, and CLI-friendly overrides without learning a new DSL.
Everything you write stays Python: functions, conditionals, list comprehensions, imports.
cfgxfocuses on loading, layering, and mutating dictionaries so you can drop the result into any workflow.
Highlights¶
- Load any Python config module with
load. - Compose configs via
parents = [...]chains or by supplying multiple paths at once. - Compute values lazily with
Lazy. - Adjust values on the fly using
apply_overridesand a compact CLI syntax. - Control merge behavior with
Delete()andReplace(value). - Snapshot final dictionaries back to Python with
dump, or pretty-print them withformat.
Core workflow¶
Start with a config file, load it, and apply CLI-style tweaks.
Define a config
Load with overrides
The result is a plain dictionary you can serialize, log, or feed into factories.
Config modules¶
Each config file is just Python. The loader only pays attention to two attributes:
config: dictionary.parents: string or list of strings pointing to other config files (paths resolved relative to the current file).
Parent chaining
You can also compose multiple files by passing a sequence of paths to load.
Multiple paths
Runtime overrides¶
Overrides can be passed to load or applied later with
apply_overrides(config_dict, sequence_of_strings), which 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 listpath-=value→ remove a matching element from a listpath!=→ 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. If parsing fails, the raw string is used, so
most string values do not need to be quoted. You can also use lazy: to define
a Lazy expression from the CLI (see Lazy values).
Assignments create intermediate dicts and extend lists with None as needed.
List indices follow Python semantics: negative indices are allowed when the list
already exists and are in range (otherwise IndexError). Deletes and list
removals are forgiving no-ops when the path is missing or out of range.
Override syntax
Merge semantics¶
When configs are layered, cfgx walks the override dictionary and combines it
with the base using:
- Dicts merge recursively.
Delete()removes the key entirely.Replace(value)usesvalueas-is without deeper merging.- Otherwise the override value replaces the base.
Delete and Replace
from cfgx 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 already
relies on it internally.
Lazy values¶
Use Lazy for values that should be computed from the merged config. A Lazy
receives c, a read-only proxy for the config where dicts are Mappings and
lists are Sequences. You can use attribute access (c.trainer.max_steps),
string keys (c["trainer"]["max_steps"]), and list indices
(c.trainer.stages[0].max_steps). String expressions can also use math and
Python builtins. Lazy values are resolved in-place after
loading (or when you call resolve_lazy) and only when they appear inside
nested dict/list structures.
Warning
The proxy references the original config values. Avoid side effects inside Lazy functions and don't rely on any specific resolution order.
Lazy with a function
Lazy from an expression
Formatting and snapshots¶
Freeze the exact configuration you ran:
Format or dump configs
formatreturns a string derived fromrepr(cfg); it defaults toprettyformatting and supportsformat="raw"for rawreproutput orformat="ruff"for Ruff formatting.dumpwrites a loadable snapshot prefixed withconfig =;dumpsreturns the same snapshot string; both default toprettyformatting and acceptformat="raw"for raw output.sort_keys=Truesorts dict keys throughout nested dict/list structures, including dict subclasses.- These are best-effort snapshots: you're responsible for
repr()being valid Python that can recreate the config. If it isn't, formatting can raise (for example, on a syntax error) orloadmay fail because required imports are missing.
Tips for structuring configs¶
- Organize by concern:
configs/base.py,configs/data/imagenet.py,configs/model/resnet.py. - Expose helper functions alongside
configfor reusable snippets. - Prefer
Lazyfor repeated derived values, e.g. a single base learning rate that feeds multiple param groups (backbone_lr = base_lr * 0.1).