This release is a pre-release and may not be stable for production use.
nshconfig
Typed Python configuration for ML runs, built on Pydantic.
nshconfig adds a small composition lifecycle to ordinary Pydantic models:
- build an incomplete, mutable draft with normal Python assignment;
- derive fields with Python callables that read validated config values;
- finalize once into a field-frozen, fully validated model;
- inspect why each value was chosen;
- save a verified, JSON-safe record of the concrete run.
There is no YAML language, registry, loader, code generator, or Pydantic re-export layer.
Documentation | Semantic contract | Changelog
Install
Version 2 is currently an alpha release, so opt in to pre-releases explicitly:
python -m pip install --pre 'nshconfig>=2.0.0a0,<3'
python -m pip install --pre 'nshconfig[treescope]>=2.0.0a0,<3' # rich notebook rendering
python -m pip install --pre 'nshconfig[transport]>=2.0.0a0,<3' # cloudpickle transport
nshconfig supports Python 3.10 through 3.14 (package metadata excludes 3.15
until it is supported) and Pydantic 2.13 through the latest Pydantic 2.x release.
Quick start
from pathlib import Path
from pydantic import Field
import nshconfig as C
class Optimizer(C.Config):
learning_rate: float = Field(default=3e-4, gt=0)
class LayerNorm(C.Config):
dim: int = C.interp(lambda context: context.parent(Model).dim)
class Model(C.Config):
dim: int = 768
norm: LayerNorm
class Run(C.Config):
optimizer: Optimizer
model: Model
work = C.draft(Run)
with C.source("large-model sweep"):
work.model.dim = 1024
work.optimizer.learning_rate = 1e-4
run = C.finalize(work)
assert run.model.norm.dim == 1024
print(C.explain(run, "model.norm.dim"))
print(C.fingerprint(run))
run_record = C.record(run)
Path("run-config.json").write_text(run_record.to_json(indent=2))
Pydantic still owns schemas, aliases, validators, constraints, serialization, and
JSON Schema. Import those APIs from pydantic; nshconfig owns only its lifecycle,
interpolation, provenance, and run-record functions.
Drafts are real instances of their config class, so normal field access remains
visible to editors and type checkers. Required fields whose annotation is one
concrete Config subclass auto-create as child drafts. Drafts cannot be serialized.
finalize() is non-destructive, so one draft can produce many sweep variants.
Direct Config fields must be required or use an interp() default. Concrete
instances, mappings, None, and default factories would create a child without an
explicit parent interpolation context, so class creation rejects them. Config
validation accepts mappings and Config instances, not attribute-based objects.
Named type aliases and string-discriminated unions preserve structural annotations;
callable union discriminators are rejected.
Interpolation reads a completed nested Config through a read-only view that
exposes declared fields only, not Pydantic methods, private state, or custom
attributes. Container slicing and arithmetic preserve protected elements and record
truthful whole-container and origin dependencies. Config classes may use before
and after field or model validators.
Model wrap, field plain/wrap, and deprecated validator decorators are rejected
because they can bypass or repeat the one-pass field lifecycle.
Finals are ordinary validated Pydantic models with field assignment disabled. This
freeze is shallow: a nested list or dict retains normal Python mutability. Every
Config is unhashable; use fingerprint() for deterministic content identity.
Calling finalize() on a final revalidates its concrete values into a fresh final,
preserves model_fields_set and provenance, and does not rerun interpolation. The
source graph is isolated before validators run, including failure paths. Finals with
identity-bearing mutable atomic values are not safely revalidatable; rebuild those
from a draft or mapping.
Composition, transport, and records
Keep the original draft when you need to change inputs and re-run interpolation. A final or JSON record contains concrete values, not the Python composition recipe.
Use cloudpickle for trusted, short-lived transport of notebook-defined classes and
drafts with interpolation callables. Use record() for durable run metadata. A v4
run record stores canonical JSON values, provenance, the concrete config type, a
schema fingerprint, an exact-runtime semantic_fingerprint, and a verified value
fingerprint. The value fingerprint binds the canonical values to both fingerprints.
Dictionary insertion order is preserved and affects identity. load_record()
validates the stored values without running interpolation. Fingerprinting first
verifies that Pydantic consumes every field and reconstructs the same runtime meaning
and JSON values, so excluded fields and lossy serializers are rejected.
The schema fingerprint covers presentation-stripped canonical and alias validation
and serialization schemas. It also records aliases for every reachable nested model
and the record identity of every reachable nested Config. It cannot detect a
validator or serializer implementation change that leaves those contracts unchanged.
A generated generic parameterization must declare a distinct record_schema_id; a
uniquely named concrete subclass already has a stable module:qualname identity and
may declare an ID to version behavior that JSON Schema cannot express. Run-record
interpolation provenance uses structural integrity tokens, rejects opaque or stale
dependency claims and impossible declaration-order edges, and requires interpolation
to be the last event for its field.
Synchronize access while fingerprinting or recording; concurrent mutation is
unsupported and observed changes fail loudly.
Structural config positions must be explicitly annotated. Lifecycle checks cover
Config nodes and exact built-in dict, list, tuple, set, and frozenset
containers. Dataclasses, ordinary Pydantic models, and other non-collection user
objects are atomic and cannot hide Config nodes or pending interpolation. Known
carrier objects, including functions, bound methods, partials, and weak references,
are inspected. Opaque callables are rejected when their captured state cannot be
inspected; other truly opaque extension state cannot be proven safe. A model-before
validator may normalize custom input, but any custom collection or lazy synchronous
or asynchronous input left afterward is rejected; materialize and await it first.
A run record's provenance must be self-contained under the recorded root.
Interpolation contexts and views expire with their resolver and cannot cross thread
boundaries. Arbitrary mapping keys use a truthful whole-mapping dependency when the
value below the key contains no Config structure.
See the semantic contract for the complete behavior, including validation order and the supported value graph.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nshconfig-2.1.0a0.tar.gz.
File metadata
- Download URL: nshconfig-2.1.0a0.tar.gz
- Upload date:
- Size: 236.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9e2eae7d62b7da95a024b95667ff1514030de5c9fcdc770b39c860e249c1d73
|
|
| MD5 |
964a0f89d95e817d990a2ea9de1b2341
|
|
| BLAKE2b-256 |
2d4755e8b2aea1759a24865ed02541ded2c63279d1c05e6b2e853f9ce7463099
|
File details
Details for the file nshconfig-2.1.0a0-py3-none-any.whl.
File metadata
- Download URL: nshconfig-2.1.0a0-py3-none-any.whl
- Upload date:
- Size: 73.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69723aaba60a598e4f1255a0d0f7ca93460b8755a7382558406cc8340a150693
|
|
| MD5 |
285d2780ec8b404307fb1775965b7eb6
|
|
| BLAKE2b-256 |
354148533406f651c12fc558dfa713995f9bcb18a0b4248b15f713a91ca8bd23
|