Skip to main content
Pre-release

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

nshconfig-2.1.0a0.tar.gz (236.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nshconfig-2.1.0a0-py3-none-any.whl (73.6 kB view details)

Uploaded Python 3

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

Hashes for nshconfig-2.1.0a0.tar.gz
Algorithm Hash digest
SHA256 a9e2eae7d62b7da95a024b95667ff1514030de5c9fcdc770b39c860e249c1d73
MD5 964a0f89d95e817d990a2ea9de1b2341
BLAKE2b-256 2d4755e8b2aea1759a24865ed02541ded2c63279d1c05e6b2e853f9ce7463099

See more details on using hashes here.

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

Hashes for nshconfig-2.1.0a0-py3-none-any.whl
Algorithm Hash digest
SHA256 69723aaba60a598e4f1255a0d0f7ca93460b8755a7382558406cc8340a150693
MD5 285d2780ec8b404307fb1775965b7eb6
BLAKE2b-256 354148533406f651c12fc558dfa713995f9bcb18a0b4248b15f713a91ca8bd23

See more details on using hashes here.

Release history Release notifications | RSS feed

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page