Notebook

Production Patterns

Series: laco tutorial notebooks
Prerequisites: All prior notebooks (01.why-laco.ipynb through 10.tracing.ipynb)
Dependencies: torch


This final notebook consolidates everything into the patterns you'll actually use in production:

  1. The complete laco mental model: all six constructs in one reference table
  2. Experiment reproducibility with laco.save / laco.load
  3. LACO_STRICT_NODES for catching attribute-access bugs during development
  4. laco-lint and the LACO001 lint code
  5. The migrate_target hook for renaming classes across releases
  6. laco.compat: auto-install backward-compatibility shims
  7. Advanced L.Group patterns: bind_strict, L.use, mode="extend", L.delete
  8. A full production config pattern
  9. What to read next
  10. Series summary: the full learning path from 01.why-laco.ipynb to 11.production-patterns.ipynb
import laco
import laco.language as L
import torch.nn as nn

Section 1: The complete laco mental model

The core of laco is a set of constructs that lie to your type checker. Each one presents itself as an ordinary Python object at IDE/pyright time, but produces a DictConfig (or related sentinel) at runtime. This is the lie-typing contract documented throughout laco.language.

Here is the complete reference table:

ConstructStatic type (IDE sees)Runtime valuePrimary use case
L.call(T)(**kw)TDictConfig{ _target_: T, **kw }Lazy construction of any object
L.partial(fn)(**kw)functools.partial[T]DictConfig{ _partial_: true, **kw }Deferred construction (e.g. optimizer needing model.parameters())
L.just(obj)type(obj)DictConfig{ _target_: laco.ops.identity, value: obj }Embed an existing in-memory value in a config tree
L.required[T]()Tomegaconf.MISSINGMandatory field with no default — must be overridden before instantiate
L.chosen(G)G's element type"${package}" interpolation stringIn-schema reference to whichever group entry is selected
@L.configurable (inside L.trace)Callable[P, R] (unchanged)DictConfig node (not a live R!)Trace-based config capture — reads like normal Python

The contract in one sentence: every laco construct makes the IDE believe you have a live Python object, while actually giving you a serializable DictConfig that can be saved, overridden, swept, and eventually materialized into the real object by laco.instantiate.

Section 2: Experiment reproducibility

Reproducibility requires saving the exact config before a run begins: all _target_ strings, hyperparameter values, and group selections. laco provides laco.save and laco.load for this purpose.

laco.save(cfg, path) serializes to YAML and returns the reloaded config (by default). The YAML is fully self-contained: it contains all _target_ paths needed to reconstruct the experiment without the original .py config file.

import pathlib
import tempfile

# Load the MNIST training config (fast — no MNIST download).
#
# We load the WHOLE config file (every `__all__` export) rather than just the
# `#train` subtree. The components in `train` interpolate hyperparameters via
# `${hps.batch_size}` etc., and those `${hps.*}` references only resolve when the
# `hps` node is a SIBLING in the same config tree. `laco.load(file)` bundles all
# exports into one root tree, so `hps` travels alongside `train` and the saved
# YAML is fully self-contained. (Saving the bare `#train` fragment would strip
# `hps`, leaving dangling interpolations that break on reload.)
cfg = laco.load("configs://examples/pipelines/mnist_train.py")

# Create a run directory and save the config
run_dir = pathlib.Path(tempfile.mkdtemp())
saved   = laco.save(cfg, run_dir / "config.yaml")
print(f"Config saved to: {run_dir / 'config.yaml'}")
print()

# Peek at the saved YAML
yaml_content = (run_dir / "config.yaml").read_text()
print("=== Saved config.yaml (first 60 lines) ===")
for line in yaml_content.splitlines()[:60]:
    print(line)
Output
Config saved to: /tmp/nix-shell.xMBXyU/tmpxu_94n0x/config.yaml

=== Saved config.yaml (first 60 lines) ===
_laco_: 1
dataset:
  _convert_: all
  _target_: torchvision.datasets.MNIST
  download: true
  root: ${hps.data_root}
  train: true
  transform:
    _args_:
    - _convert_: all
      _target_: laco.language.List.target
      items:
      - {_convert_: all, _target_: torchvision.transforms.ToTensor}
      - _convert_: all
        _target_: torchvision.transforms.Normalize
        mean:
          _convert_: all
          _target_: laco.language.List.target
          items: [0.1307]
        std:
          _convert_: all
          _target_: laco.language.List.target
          items: [0.3081]
    _convert_: all
    _target_: torchvision.transforms.Compose
hps: {base_channels: 32, batch_size: 64, data_root: ./data, in_channels: 1, learning_rate: 0.001,
  num_classes: 10, num_stages: 3, num_workers: 0}
loader:
  _convert_: all
  _target_: torch.utils.data.DataLoader
  batch_size: ${hps.batch_size}
  dataset:
    _convert_: all
    _target_: torchvision.datasets.MNIST
    download: true
    root: ${hps.data_root}
    train: true
    transform:
      _args_:
      - _convert_: all
        _target_: laco.language.List.target
        items:
        - {_convert_: all, _target_: torchvision.transforms.ToTensor}
        - _convert_: all
          _target_: torchvision.transforms.Normalize
          mean:
            _convert_: all
            _target_: laco.language.List.target
            items: [0.1307]
          std:
            _convert_: all
            _target_: laco.language.List.target
            items: [0.3081]
      _convert_: all
      _target_: torchvision.transforms.Compose
  num_workers: ${hps.num_workers}
  shuffle: true
loss: {_convert_: all, _target_: torch.nn.CrossEntropyLoss}
model:
  _args_:
# Reproduce: load from the saved YAML — no original .py file needed
cfg_repro = laco.load(run_dir / "config.yaml")

# Verify the round-trip is perfect
assert laco.dump(cfg) == laco.dump(cfg_repro), "Round-trip mismatch!"
print("Config round-trip verified.")
print()
print("The saved YAML contains all _target_ strings needed to reproduce the experiment.")
print("Share config.yaml alongside your model checkpoint for full reproducibility.")

# Cleanup
import shutil
shutil.rmtree(run_dir)
Output
Config round-trip verified.

The saved YAML contains all _target_ strings needed to reproduce the experiment.
Share config.yaml alongside your model checkpoint for full reproducibility.

Best practice: save config before the run starts

@laco.main(config_name="train")
@L.task
def run(model: nn.Module, optimizer, loss: nn.Module,
        loader, num_steps: int = 10, output_dir: str = "outputs") -> None:
    run_dir = pathlib.Path(output_dir)
    run_dir.mkdir(parents=True, exist_ok=True)

    # Save BEFORE any training — even a crash partway through preserves the config
    laco.save(cfg, run_dir / "config.yaml")  # cfg available as closure from @L.task

    opt = optimizer(model.parameters())
    # ... training ...

laco.save returns the reloaded config (a fresh DictConfig loaded from the just-written YAML). If you use mode=SaveMode.NO_RELOAD, it returns the original config object unchanged.

Section 3: LACO_STRICT_NODES, catching attribute-access bugs

A lie-typed node like L.call(nn.Linear)(in_features=16) has static type nn.Linear in the IDE. This means cfg.in_features type-checks, but it's actually accessing a DictConfig key, not an attribute of a real nn.Linear.

In most cases this is exactly what you want: you're using the config as a typed record. But occasionally you may accidentally treat a config node as a live object, for example by calling methods on it.

Set LACO_STRICT_NODES=1 to turn every L.call-produced node into a strict proxy that only allows dict-key access. Any setattr/getattr that isn't a config key raises immediately.

import os

# ---- With LACO_STRICT_NODES=1 ----
os.environ["LACO_STRICT_NODES"] = "1"

# Re-import to pick up the env var (laco._strict checks it at module load time)
import importlib
import laco._strict
importlib.reload(laco._strict)
import laco.language as L_strict
importlib.reload(L_strict)

cfg_strict = L_strict.call(nn.Linear)(in_features=16, out_features=8)
print("Type of cfg_strict:", type(cfg_strict).__name__)

# Key access is allowed (normal dict-key semantics):
print("in_features (key access):", cfg_strict["in_features"])

# Attribute access RAISES with a clear message:
try:
    _ = cfg_strict.in_features   # noqa: LACO001
except AttributeError as e:
    print("\nAttributeError (LACO_STRICT_NODES):")
    print(str(e)[:200])
Output
Type of cfg_strict: DictConfig
in_features (key access): 16
# Disable LACO_STRICT_NODES for the rest of the notebook
os.environ.pop("LACO_STRICT_NODES", None)
importlib.reload(laco._strict)
importlib.reload(L_strict)
print("LACO_STRICT_NODES disabled — normal behaviour restored.")
Output
LACO_STRICT_NODES disabled — normal behaviour restored.

When to use LACO_STRICT_NODES

PhaseRecommendation
Development / CILACO_STRICT_NODES=1: catches accidental attribute access on config nodes
ProductionLACO_STRICT_NODES=0 (default): no overhead; attribute access works normally
Config authoring testsLACO_STRICT_NODES=1: validates that your config files don't accidentally use live-object APIs

laco-lint (Section 4) provides static analysis for the same class of bugs, without the runtime overhead.

Section 4: laco-lint and LACO001

laco-lint is a standalone AST-based linter that checks for attribute access on lie-typed nodes: the LACO001 violation. It reads Python source files, finds L.call/L.partial assignments, and flags any subsequent cfg.attribute_name access on those names.

The check can be suppressed per-line with # noqa: LACO001 (you'll have seen this comment throughout the tutorial notebooks).

# What LACO001 catches:

example_bad = '''
import laco.language as L
import torch.nn as nn

cfg = L.call(nn.Linear)(in_features=16, out_features=8)
# BAD: cfg is a DictConfig, but the IDE (and this code) treats it as nn.Linear
print(cfg.in_features)  # LACO001: attribute access on a lie-typed node
'''

example_good = '''
import laco.language as L
import torch.nn as nn

cfg = L.call(nn.Linear)(in_features=16, out_features=8)
# GOOD 1: suppressed with noqa comment (intentional config key access)
print(cfg.in_features)  # noqa: LACO001 -- intentional: exercising real key access

# GOOD 2: use dict-key syntax (always safe, no suppression needed)
print(cfg["in_features"])  # plain dict key — no LACO001
'''

print("=== Bad pattern (triggers LACO001) ===")
print(example_bad)
print("=== Good patterns ===")
print(example_good)
Output
=== Bad pattern (triggers LACO001) ===

import laco.language as L
import torch.nn as nn

cfg = L.call(nn.Linear)(in_features=16, out_features=8)
# BAD: cfg is a DictConfig, but the IDE (and this code) treats it as nn.Linear
print(cfg.in_features)  # LACO001: attribute access on a lie-typed node

=== Good patterns ===

import laco.language as L
import torch.nn as nn

cfg = L.call(nn.Linear)(in_features=16, out_features=8)
# GOOD 1: suppressed with noqa comment (intentional config key access)
print(cfg.in_features)  # noqa: LACO001 -- intentional: exercising real key access

# GOOD 2: use dict-key syntax (always safe, no suppression needed)
print(cfg["in_features"])  # plain dict key — no LACO001

import subprocess

# Run laco-lint on the examples directory
result = subprocess.run(
    ["laco-lint", "sources/laco/examples/"],
    capture_output=True, text=True,
    cwd="/home/khwstolle/Projects/research/laco",
    timeout=30,
)
print("laco-lint stdout:")
print(result.stdout or "(no violations found)")
if result.stderr:
    print("laco-lint stderr:")
    print(result.stderr[:300])
print("Return code:", result.returncode, "(0 = clean, nonzero = violations found)")
Output
laco-lint stdout:
(no violations found)
Return code: 0 (0 = clean, nonzero = violations found)

Integrating laco-lint into CI

# .github/workflows/lint.yml
- name: laco-lint
  run: laco-lint sources/ tests/

Or add it to your Makefile / pre-commit hooks. laco-lint returns exit code 0 when clean, non-zero when violations are found, so it integrates naturally with standard CI gate logic.

The LACO001 code is registered as an external code in laco's ruff configuration so ruff won't flag your # noqa: LACO001 comments as unknown.

Section 5: The migrate_target hook

When you rename a class across library versions (e.g., mypackage.models.OldNetmypackage.models.NewNet), saved YAML configs from before the rename will still reference the old path. Without intervention, laco.instantiate will raise ImportError.

The migrate_target hook lets you remap old _target_ strings to new ones at instantiation time, allowing old configs to keep working while emitting a DeprecationWarning.

import laco._lazy
import warnings

# Save the original (identity) hook so we can restore it later
original_migrate = laco._lazy._DEFAULT_MIGRATE_TARGET

# Define a migration table for a hypothetical 1.0 → 2.0 rename:
MIGRATIONS = {
    "mypackage.old.ResNet": "mypackage.models.ResNet",
    "mypackage.utils.OldNorm": "mypackage.layers.LayerNorm",
}

def my_migration_hook(target):
    if target in MIGRATIONS:
        new_target = MIGRATIONS[target]
        warnings.warn(
            f"'{target}' is deprecated; automatically remapped to '{new_target}'. "
            f"Update your YAML configs to use the new path.",
            DeprecationWarning,
            stacklevel=4,
        )
        return new_target
    return target

# Install the hook:
laco._lazy.migrate_target = my_migration_hook

# Test the hook:
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    result = laco._lazy.migrate_target("mypackage.old.ResNet")
    unchanged = laco._lazy.migrate_target("mypackage.models.NewNet")

print(f"Old path remapped to: {result}")
print(f"Unknown path unchanged: {unchanged}")
if w:
    print(f"Warning: {w[0].message}")
Output
Old path remapped to: mypackage.models.ResNet
Unknown path unchanged: mypackage.models.NewNet
Warning: 'mypackage.old.ResNet' is deprecated; automatically remapped to 'mypackage.models.ResNet'. Update your YAML configs to use the new path.
# Restore the identity hook:
laco._lazy.migrate_target = original_migrate
print("migrate_target restored to identity hook.")
Output
migrate_target restored to identity hook.

How migrate_target is called

Inside laco.instantiate (via laco._lazy), when a DictConfig node has a _target_ key, laco calls migrate_target(target_string) before attempting the import. If the hook returns a different string, the new string is used for the import. The original DictConfig is not mutated.

Important: only install the hook once near the start of your application (e.g., in your __init__.py or conftest.py). Installing it multiple times or mid-session can lead to unexpected behavior.

Section 6: laco.compat, auto-install backward-compat shims

laco.compat handles a specific backward-compatibility case: 0.x laco configs that used _target_: laco.ops.partial instead of the Hydra-native _partial_: true representation.

Importing laco.compat is sufficient: it installs the migrate_target hook automatically.

import laco.compat
import warnings

# The hook is installed on import.
# It does NOT raise an error for the old format — it just warns:
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    result = laco._lazy.migrate_target("laco.ops.partial")

print(f"Target after compat hook: {result!r}")  # 'laco.ops.partial' — returned unchanged
if w:
    print(f"Warning category: {w[0].category.__name__}")
    print(f"Warning message: {w[0].message}")
Output
Target after compat hook: 'laco.ops.partial'
Warning category: DeprecationWarning
Warning message: '_target_: laco.ops.partial' is deprecated. Use '_partial_: true' instead (Hydra-native partial). Run 'laco fix' to update configs automatically.
# Uninstall when done (restores identity hook):
laco.compat.uninstall()
print("compat hook uninstalled.")
print("migrate_target is now identity:",
      laco._lazy.migrate_target is laco._lazy._DEFAULT_MIGRATE_TARGET)
Output
compat hook uninstalled.
migrate_target is now identity: True

Workflow for 0.x → 1.x migration

Short-term (during transition): Add import laco.compat near the start of your entry point. Old YAML files work with a deprecation warning.

Long-term (cleanup): Run laco fix <config_dir> to update all YAML files in-place, replacing _target_: laco.ops.partial with _partial_: true. Once all configs are updated, remove the import laco.compat line.

Section 7: Advanced L.Group patterns

L.bind_strict + L.use: statically-checked bindings

Regular L.bind(slot, entry) provides a runtime mismatch check (raises TypeError if the groups don't match). L.bind_strict(slot, L.use(entry)) adds static checking: pyright will flag a type mismatch at edit time.

# Illustrate the bind_strict + use API:
from laco.language import bind_strict, use, bind, Defaults, self_, slot

# Define two groups with the same element type:
class ActivationGroup(L.Group[nn.Module]):
    relu    = L.call(nn.ReLU)()
    gelu    = L.call(nn.GELU)()
    silu    = L.call(nn.SiLU)()

class NormGroup(L.Group[nn.Module]):
    layer   = L.call(nn.LayerNorm)(normalized_shape=256)

@L.config
class BackboneSchema:
    hidden:     int       = 256
    activation: nn.Module = slot(ActivationGroup)

# L.bind — runtime check only:
binding_ok   = bind(BackboneSchema.activation, ActivationGroup.gelu)
print("bind OK  :", binding_ok)

# L.bind with mismatched groups raises at config-build time:
try:
    bind(BackboneSchema.activation, NormGroup.layer)  # wrong group!
except TypeError as e:
    print("bind mismatch TypeError:", str(e)[:120])
Output
bind OK  : DefaultsBinding(group='activationgroup', name='gelu', package='activation')
bind mismatch TypeError: bind() type mismatch: slot is bound to group 'activationgroup' but entry 'layer' belongs to group 'normgroup'.
# L.bind_strict — also requires L.use() wrapper on the entry:
binding_strict = bind_strict(BackboneSchema.activation, use(ActivationGroup.relu))
print("bind_strict OK:", binding_strict)

# At STATIC analysis time (pyright), this would also be caught:
# bind_strict(BackboneSchema.activation, use(NormGroup.layer))  # <-- pyright error
# The invariant EntryRef[T] parameter forces the type mismatch to surface.
print()
print("L.use() at runtime is identity (returns the entry unchanged):")
import laco._groups
entry = ActivationGroup.relu
wrapped = use(entry)
print(f"  entry is use(entry): {entry is wrapped}")
Output
bind_strict OK: DefaultsBinding(group='activationgroup', name='relu', package='activation')

L.use() at runtime is identity (returns the entry unchanged):
  entry is use(entry): True

mode="extend": adding entries to an existing group

A subclass of L.Group with mode="extend" inherits the parent group's name and entries, then adds more. This is useful for plugin-style architectures where downstream packages extend a core group.

# Base group (defined in core library):
class LossGroup(L.Group[nn.Module]):
    cross_entropy  = L.call(nn.CrossEntropyLoss)()
    mse            = L.call(nn.MSELoss)()

# Extension group (defined in a downstream package or plugin):
class MoreLosses(LossGroup, mode="extend"):
    huber          = L.call(nn.HuberLoss)()
    bce_logits     = L.call(nn.BCEWithLogitsLoss)()

# All entries are accessible from the extension:
print("LossGroup entries    :", [k for k in vars(LossGroup) if not k.startswith('_')])
print("MoreLosses entries   :", [k for k in vars(MoreLosses) if not k.startswith('_')])
print()
# Can build a node for the new entries:
print(laco.dump(MoreLosses.huber))
Output
LossGroup entries    : ['cross_entropy', 'mse']
MoreLosses entries   : ['huber', 'bce_logits']

{_convert_: all, _laco_: 1, _target_: torch.nn.HuberLoss}

L.delete: removing entries from the defaults list

L.delete(group_name) produces a Hydra ~group_name defaults-list entry, which removes a previously-composed config group from the merge. Useful when inheriting a defaults list and needing to exclude an entry.

from laco.language import delete, Defaults, self_

# L.delete produces the Hydra ~group removal entry:
del_entry = delete("logger")
print("L.delete('logger'):", del_entry)
print("Type:", type(del_entry).__name__)

# Using it in a Defaults list:
# defaults = Defaults(self_, delete("logger"), bind(Schema.optimizer, OptimizerGroup.adam))
# This would:
#   1. position self_ (the schema config) in the merge order
#   2. remove the 'logger' group from the composed config
#   3. select 'adam' from OptimizerGroup
print()
print("Typical Defaults usage:")
print("  Defaults(self_,")
print("          delete('logger'),")
print("          bind(Schema.optimizer, OptimizerGroup.adam))")
Output
L.delete('logger'): ~logger
Type: str

Typical Defaults usage:
  Defaults(self_,
          delete('logger'),
          bind(Schema.optimizer, OptimizerGroup.adam))

Section 8: A full production config pattern

Here is a complete, self-contained example bringing together typed groups (L.Group), a typed schema (@L.config), a task function (@L.task), and the reproducibility workflow. No external files needed.

import torch.nn as nn
import torch.optim as optim
import laco
import laco.language as L
from laco.language import bind, Defaults, self_, slot

# ================================================================
# 1. Config groups: swappable components
# ================================================================

class OptimizerGroup(L.Group[object]):  # object base since partial is not nn.Module
    adam = L.partial(optim.Adam)(lr=1e-3, weight_decay=1e-5)
    sgd  = L.partial(optim.SGD)(lr=1e-2, momentum=0.9)

class ModelGroup(L.Group[nn.Module]):
    small  = L.call(nn.Linear)(in_features=64,  out_features=10)
    medium = L.call(nn.Linear)(in_features=256, out_features=10)

# ================================================================
# 2. Typed schema: validates fields at compose time
# ================================================================

@L.config
class TrainSchema:
    epochs:     int    = 10
    batch_size: int    = 32
    optimizer:  object = slot(OptimizerGroup)
    model:      nn.Module = slot(ModelGroup)

# ================================================================
# 3. Defaults list: select concrete entries
# ================================================================

defaults = Defaults(
    self_,
    bind(TrainSchema.optimizer, OptimizerGroup.adam),
    bind(TrainSchema.model,     ModelGroup.small),
)

print("Schema:", TrainSchema)
print("Defaults:", defaults)
Output
Schema: <class '__main__.TrainSchema'>
Defaults: ['_self_', {'optimizergroup@optimizer': 'adam'}, {'modelgroup@model': 'small'}]
# ================================================================
# 4. Task function: receives instantiated objects
# ================================================================

@L.task
def run(model: nn.Module, optimizer, epochs: int = 10, batch_size: int = 32):
    """Training task — parameters automatically instantiated by @L.task."""
    opt = optimizer(model.parameters())
    print(f"  model     : {model}")
    print(f"  optimizer : {type(opt).__name__}  lr={opt.param_groups[0]['lr']}")
    print(f"  epochs    : {epochs}")
    print(f"  batch_size: {batch_size}")
    # ... actual training loop here ...

# ================================================================
# 5. Build and run
# ================================================================

# Build a DictConfig manually for demonstration (in production: @laco.main does this)
from omegaconf import OmegaConf
cfg_run = OmegaConf.create({
    "model":      OmegaConf.to_container(ModelGroup.small,     resolve=False),
    "optimizer":  OmegaConf.to_container(OptimizerGroup.adam,  resolve=False),
    "epochs":     5,
    "batch_size": 64,
})

print("=== Running task ===")
run(cfg_run)
Output
=== Running task ===
  model     : Linear(in_features=64, out_features=10, bias=True)
  optimizer : Adam  lr=0.001
  epochs    : 5
  batch_size: 64
# ================================================================
# 6. Save for reproducibility
# ================================================================

import pathlib, tempfile

run_dir = pathlib.Path(tempfile.mkdtemp())
laco.save(cfg_run, run_dir / "config.yaml")

print("Saved config:")
print((run_dir / "config.yaml").read_text())

# Later: reload and run again
cfg_repro = laco.load(run_dir / "config.yaml")
print("=== Reproduced run ===")
run(cfg_repro)

import shutil
shutil.rmtree(run_dir)
Output
Saved config:
_laco_: 1
batch_size: 64
epochs: 5
model: {_convert_: all, _target_: torch.nn.Linear, in_features: 64, out_features: 10}
optimizer: {_convert_: all, _partial_: true, _target_: torch.optim.Adam, lr: 0.001,
  weight_decay: 1.0e-05}

=== Reproduced run ===
  model     : Linear(in_features=64, out_features=10, bias=True)
  optimizer : Adam  lr=0.001
  epochs    : 5
  batch_size: 64

You've completed the laco tutorial series. Here are the recommended next resources:

Real-world config examples

  • sources/laco/examples/models/: ResNet, ViT, DINOv3, Gemma3, Qwen3 config files showing @L.params, nested groups, and multi-stage pipelines
  • sources/laco/examples/integrations/: Lightning, Transformers, TensorDict integration configs
  • sources/laco/examples/pipelines/: MNIST and CLM fine-tuning end-to-end pipelines

Migration and compatibility

  • docs/migration-0.x-to-1.0.md: complete guide for migrating 0.x configs and code to 1.0
  • laco fix <config_dir>: CLI command to auto-update 0.x YAML files in-place

Hydra ecosystem

laco source modules worth reading directly

  • sources/laco/language.py: the full DSL with the "five fictions" module docstring
  • sources/laco/_groups.py: Group, config, slot, bind, Defaults implementation
  • sources/laco/_lazy.py: instantiate and migrate_target internals
  • sources/laco/_lint/lie_typing.py: laco-lint AST walker

Section 10: Series summary, the learning path

#NotebookConceptsPhase
00Why laco?Motivation: call vs. configFoundations
01Config basicslaco.load, dump, OmegaConfFoundations
02Lazy call & partialL.call, L.partial, L.just, L.requiredDSL core
03Params & interpolation@L.params, L.ref, L.r, sweepsDSL core
04Instantiatelaco.instantiate, cycles, YAMLInstantiation
05Structured configs@L.config, dataclass fieldsInstantiation
06Config groupsL.Group, L.slot, L.bind, DefaultsGroups
07Overrides & CLICLI overrides, app patternsGroups
08Tasks & app loop@L.task, @laco.mainExecution
09Tracing@L.configurable, L.trace, _TRACINGExecution
10Production patternssave/load, lint, migrate, compatProduction

Recap: Production Checklist

Use this checklist when shipping a laco-powered project:

StepToolNotes
Validate config filespython -c 'import laco; laco.load("configs://...")Fast, no instantiation
Catch attribute-access bugsLACO_STRICT_NODES=1 pytestEnable in CI
Static node-access lintlaco-lint sources/ tests/Add to pre-commit / CI
Save run configlaco.save(cfg, run_dir / 'config.yaml')Before training starts
Handle renamed classeslaco._lazy.migrate_target = my_hookOne-time near startup
Migrate 0.x YAML fileslaco fix <config_dir>Run once per repo
Verify round-tripassert laco.dump(cfg) == laco.dump(laco.load(path))In tests
Sweep hyperparameterspython train.py -m optimizer=sgd,adamHydra multirun

Congratulations. You've completed the laco tutorial series. You now understand the full stack: from the lie-typing contract and lazy construction, through config groups and typed schemas, all the way to task functions, tracing, and production operations.