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. - Update values from previous layers with
Update. - 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 and update: to define an Update expression from the CLI
(see Lazy values and Update 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.Update(fn_or_expr)transforms the previous value at that path.- 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.
Update values¶
Use Update when a child layer should transform the previous value at the same
path.
Update applies during merge:
- If the previous value is concrete, the update runs immediately.
- If the previous value is
Lazy, cfgx composes a newLazyand the update runs when that lazy resolves.
Update can be defined from a callable (lambda v: ...) or a string
expression ("v + [1]"). String expressions get v and math.
Update from the previous value
Note
For missing keys, callable updates are invoked with no argument, so a
callable default like lambda v=[]: v + ["x"] works. String updates
require an existing value.
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). - Prefer
Updatewhen changing an inherited value at the same key, especially for lists and incremental numeric adjustments.