Quickstart
Define Pydantic schemas
Import nshconfig once and subclass C.Config. Pydantic authoring APIs are
available from the same namespace:
import nshconfig as C
class Optimizer(C.Config):
learning_rate: float = C.Field(default=3e-4, gt=0)
"""Optimizer step size."""
class Norm(C.Config):
dim: int = C.interp(lambda context: context.parent(Model).dim)
class Model(C.Config):
dim: int = 768
norm: Norm = Norm()
class Run(C.Config):
seed: int
optimizer: Optimizer = Optimizer()
model: Model = Model()
Norm() first attempts ordinary validation. Its missing parent context produces
an inert unbound template, whose raw constructor recipe binds inside Model’s
normal Pydantic field pipeline. No factory syntax is needed. Optimizer() and
Model() are validated finals with replayable default recipes. All three become
fresh child drafts when their parent default is projected into a draft.
Validation is strict by default, and attribute docstrings become field
descriptions when class source is available. Factories returning drafts or
templates, including C.Field(default_factory=Norm.config_draft), are rejected;
use direct Norm() defaults.
Compose a draft
work = Run.config_draft()
work.seed = 7
work.optimizer.learning_rate = 1e-4
work.model.dim = 1024
assert C.is_draft(work)
assert C.is_draft(work.model)
Draft assignment does not validate. Required Config fields lazily create child
drafts. Reading another missing required field raises C.UnsetError.
Finalize once
run = work.config_finalize()
assert run.seed == 7
assert run.model.dim == 1024
assert run.model.norm.dim == 1024
assert not C.is_draft(run)
Finalization recursively collects the declared Config graph, evaluates interpolation, and runs normal Pydantic validation. It is non-destructive:
work.model.dim = 2048
larger = work.config_finalize()
assert run.model.norm.dim == 1024
assert larger.model.norm.dim == 2048
Use ordinary Pydantic construction
Users who do not need draft composition can construct the same schema normally:
direct = Run(
seed=7,
optimizer=Optimizer(learning_rate=1e-4),
model=Model(dim=1024),
)
assert direct == run
Pydantic owns aliases, validators, serialization, and JSON Schema:
payload = run.model_dump()
schema = Run.model_json_schema()
checked = Run.model_validate(payload)
Draft and template serialization is rejected, including through TypeAdapter
and nested Pydantic serializers.
Build project presets
Reusable presets and root experiment files are both ordinary mutators:
def large_model(cfg: Model, *, dim: int = 2048) -> Model:
cfg.dim = dim
return cfg
def __config__(cfg: Run) -> Run:
large_model(cfg.model)
cfg.seed = 11
return cfg
See project composition for the endorsed directory layout and application-side identity check.