~/problems / Iterators & parsers / Parsers and interpreters

ML config loader: inherit, override, interpolate, validate

medium 4 levels ~80 min Anthropic

Level 1 Composing configs by inheritance

You're writing the loader for a training-run config system (think a tiny Hydra/OmegaConf). A config is a nested dict of JSON-like values. Configs are kept in a registry configs: dict[str, dict] and may inherit from others.

Implement resolve(configs, name) -> dict and an exception class ConfigError(ValueError):

  • The optional key "_base_" in a config is a parent name or a list of parent names. The result starts empty; each parent's fully resolved config is merged on in list order; finally the config's own keys are merged on top. "_base_" never appears in the output.
  • Deep merge: when both sides have a dict under the same key, merge them recursively. In every other case (scalars, lists, a dict meeting a non-dict) the later value simply replaces the earlier one. Lists are not concatenated.
  • Raise ConfigError if a name isn't in the registry or if inheritance loops back on itself.
  • Never mutate the inputs, and don't let the result share mutable objects with them.
configs = {
    "base":  {"optim": {"name": "adam", "lr": 0.001}, "layers": [64, 64]},
    "big":   {"_base_": "base", "layers": [256, 256, 256]},
    "sweep": {"_base_": ["big"], "optim": {"lr": 0.01}},
}
resolve(configs, "sweep")
# {"optim": {"name": "adam", "lr": 0.01}, "layers": [256, 256, 256]}

Parents can be shared (diamonds), and chains can be long. Resolve each named config once per call, not once per path that reaches it.

Level 2 unlocks when level 1 passes.

Level 3 unlocks when level 2 passes.

Level 4 unlocks when level 3 passes.

Topic: Parsers and interpreters. Tokenize, recursive descent, S-expressions, evaluation and type inference.

0:00
Ctrl ' run · Ctrl ↵ submit
esc