Expressions¶
Select a value¶
from cfgx import final, previous, value
config = {
"lr": value * 0.1,
"backbone_lr": final.lr * 0.1,
"inherited_width": previous.model.width,
}
final includes all layers and CLI overrides. previous includes definitions
before the expression's originating layer. value is previous(0): the inherited
value at the expression's output location. Earlier definitions keep their own
origins, and their final references still see the complete composition.
Inside a nested compose, previous also includes preceding local contributions
at that location. Surrounding sibling fields retain their enclosing layer's
previous view; see the mental model.
Attributes select dictionary keys. Subscripts support arbitrary hashable keys
and sequence indices; final[("a", "b")] selects a tuple key, not two path parts.
Use subscripts for keys that are not Python identifiers or conflict with Python
special attributes. Sequences support negative indices and slices.
Both roots accept a parent count:
| Expression | Starting location |
|---|---|
final or final(None) |
Config root |
final(0) |
Current output location |
final(1) |
Immediately containing container |
final(2) |
Two path levels above the output location |
previous(n) |
Same location, using earlier definitions |
Dictionary keys and sequence indices each count as one level. Expression operators and maps do not add levels. Going above the root errors.
block = {"width": 64, "hidden": final(1).width * 2}
config = {"encoder": block, "decoder": block}, {"decoder": {"width": 128}}
Here the hidden widths are 128 and 256. Reusing a declaration binds it separately at each output location.
Transform and combine¶
Arithmetic and comparisons build expressions. Use .map for ordinary Python
functions:
config = {
"warmup_steps": (final.steps * 0.025).map(int),
"tags": value.default([]).map(lambda x: [*x, "finetune"]),
"scaled": final.lr.map("x * 2"),
}
The string form is a Python expression with x as its resolved input. It also
provides the expression API, math, and normal builtins. This is trusted Python,
just like the code in a config file.
For conditional or multi-value computations:
from cfgx import computed, delete, final
config = {
"checkpoint": computed(
lambda get: get(final.checkpoint_path) if get(final.resume) else delete
),
"effective_batch": computed(
lambda get: int(get(final.batch_size)) * get(final.accumulation)
),
}
get takes any expression and returns its fully resolved value, with supported
containers structurally copied. Only reads actually executed create dependencies.
get(final.lr.map("x * 2")) is valid. A callback may return nested expressions,
replace, delete, include, or compose. Returned values bind at their output
locations within the same originating layer; explicit compositions introduce
ordered local layers. Includes returned by a callback resolve paths relative to
the expression's declaring file. Returned strings and tuples remain data.
Unresolved expressions have no Python truth value and cannot be iterated.
Use get inside computed for if, and, or, comprehensions, and functions
that require ordinary values. Expressions are declarations, not container proxies.
Missing values¶
final.optimizer.default({"lr": 1e-3}).lr.default(1e-4)
final.optional.default(final.fallback)
computed(lambda get: delete).default(None)
Defaults apply to direct absence, including deletion. None, False, and zero
are present values. Fallbacks are evaluated only when needed and do not merge
into an existing dictionary. Defaults do not suppress callback exceptions,
missing dependencies inside another definition, or cycles. An unresolved missing
reference without a default raises MissingValueError.
Read structure precisely¶
.keys()returns a list of mapping keys, after computed deletions..len()returns length without materializing supported sequence elements or dictionary child contents. Dictionary membership must still be determined..exists()checks presence without materializing a present subtree.
from cfgx import computed, value
config = {
"datasets": computed(
lambda get: {
name: {"image_augmentation": []}
for name in get(value.keys())
}
),
}
This returns a partial patch. It preserves other dataset fields without reading
their values. In contrast, value.map(fn) materializes the whole inherited
subtree before calling fn.
If a callback may delete an entry, it may need to execute before that entry's
presence is known. A callback producing an entire dictionary must execute to
reveal its keys. Structural reads cannot skip Python statements inside callbacks.
expr.map(len) materializes the input; expr.len() requests only its length.
Names remain available as keys: final.map selects "map", final.map(fn)
transforms the root, and final.map.map(fn) transforms the "map" value. The
same distinction applies to default, keys, len, and exists.
Caching and cycles¶
Within one load, each computation runs at most once per expression binding, output location, and originating layer. Structure and full-value reads share producer results. A fresh load computes fresh results. Opaque-object mutations and external side effects remain ordinary Python behavior; evaluation order is not a public guarantee.
Cycles raise ConfigError with a path, layer, and operation trace. Reading your
own full final value, including through an ancestor, creates a cycle. Earlier
values can also cycle when they contain final references back to the calculation.
A keys-only read can succeed where a full read would cycle, but computed deletion can itself make membership cyclic. The reweighting example reads names, weights, and label types explicitly, then patches just the weights. This leaves paths, augmentations, and other dataset settings intact.
Opaque objects¶
Explicit selectors can read through an opaque object's __getitem__ interface.
The opaque object is resolved as one dependency, then ordinary Python access
applies. Attribute syntax remains key sugar, not object attribute lookup.
.keys() and .len() use its normal methods. cfgx does not discover declarations
hidden inside opaque objects. Build custom objects from resolved inputs when
needed, for example computed(lambda get: MyOptions(get(final.options))).