Python binding
The Python binding exposes the SNT modules through the scinumtools3
package. It is built on the same C++ implementation used by the command-line
and CMake interfaces, so PUQ and DIP expressions have the same semantics in
Python and C++.
After installation, import the module or one of its submodules:
Quantities and units
PUQ represents numerical values together with their physical units and, when needed, uncertainties. Quantities can be added, multiplied, compared, formatted, and converted between compatible unit systems while retaining dimensional information. Use it when calculations should prevent accidental mixing of incompatible units.
from scinumtools3.puq import Quantity
from scinumtools3.dip import DIP
length = Quantity(2.5, "m")
print(length.convert("cm"))
DIPL parameters
The Python binding can load a DIPL definition from a file, evaluate its
parameters, and return a value in Python. For example, given a file named
parameters.dip:
length float = 2.5 dm
width float = 40 mm
area float = ( {?length} * {?width} ) m2
Parse it into an environment with DIP.add_file:
from pathlib import Path
from scinumtools3.dip import DIP
dip = DIP()
dip.add_file(Path("parameters.dip"))
env = dip.parse()
For a reusable DIPfile manifest, call add_project instead of adding each
file, source, and unit individually:
dip = DIP()
dip.add_project("DIPfile")
env = dip.parse()
See DIPfile projects for the manifest format.
Application adapters
Subclass Adapter to turn an evaluated environment into one or more
application-specific files. The Python callback receives the same registered
output types as C++: text, binary bytes, and a streamed write(bytes)
function. run_adapter_project parses a DIPfile; run_adapter_snapshot
loads a DIPH5 snapshot; run_adapter accepts an existing environment.
from scinumtools3.dip import Adapter, run_adapter_project
class AnalysisAdapter(Adapter):
def plan(self, env, context):
steps = env["run.steps"].value
context.add_text("job.json", '{"steps": %d}\n' % steps)
context.add_binary("marker.bin", b"SNT3")
def write_steps(write):
for i in range(steps):
write(f"{i}\n".encode())
context.add_stream("steps.csv", write_steps)
run_adapter_project("DIPfile", AnalysisAdapter(), "analysis-inputs")
See Application adapters for path rules and the complete C++ and Python examples.
Accessing nodes
There are two main ways to access nodes:
Cursor: use
env["path"]to inspect a known path or traverse groups and collections in the environment.Select: use
env.select(...)to discover value nodes by path and tags and inspect independent snapshots of the results.
Cursor: inspect a known path
area = env["area"]
print(area.value, area.units) # 0.01 m2
print(area.metadata.description)
A cursor accesses a path in the environment. Use cursor.elements() for
list elements and cursor.items() for named items in a map. It also exposes
shape, metadata, provenance, and to_numpy().
The cursor also provides values and units for unitless scalars. For example,
a file containing count int = 42 can be queried with
env["count"].value. The cursor returned by env["area"] exposes both
value and units; pass the value to Quantity when an explicit unit
conversion is needed:
from scinumtools3.puq import Quantity
area_in_cm2 = Quantity(area.value, area.units.to_string()).convert("cm2")
print(area_in_cm2) # 100 cm2
Select: discover and inspect nodes
Select returns value nodes whose values, units, tags, and metadata can be inspected directly:
dip = DIP()
dip.add_string('''physics
speed float = 2 m/s
!tags ["export", "runtime", "hydro"]
?descr "Flow speed"
''')
env = dip.parse()
for node in env.select(
"?physics.",
tags_all=["export", "runtime"],
tags_any=["hydro", "gravity"],
tags_none=["internal", "deprecated"],
):
print(node.name, node.value, node.units, node.tags, node.metadata.description)
tags_all requires every listed tag, tags_any requires at least one,
and tags_none excludes nodes with any listed tag. The filters combine
with AND; omitted or empty filters impose no restriction. Tags match only
explicit assignments, without parent-tag inheritance.
? selects all value nodes, ?physics. selects a subtree including its
value-bearing root and collection members, and ?physics.speed selects
an exact node. Results retain fully qualified paths in environment order,
with each matching node returned once. No matches returns an empty list.
Selected nodes are independent snapshots; selection does not modify the
environment. Their tags and metadata properties are read-only.
Changing a returned tags list affects only that list. Metadata remains valid
while its Python wrapper is alive.
Additional request helpers remain available in the Python DIP API reference.
Registering schemas
Register a schema body directly, then apply it in ordinary DIPL code:
dip = DIP()
dip.add_schema_string("settings", "speed float = 2 m/s\n")
# Alternatively: dip.add_schema_file("settings", Path("settings.dipl"))
dip.add_string("physics : settings")
env = dip.parse()
Bodies start at indentation zero and omit the $schema wrapper. Registration
preserves source information and leaves value evaluation to schema application.
Put schema-level ? metadata before the first body node, then inspect it with
env.schemas["settings"].metadata. Schema metadata describe the definition
and are not copied onto the applying group, collection, item, or value nodes.
DIPH5 stores evaluated nodes and descriptive schema provenance rather than
reusable schema definitions. After loading a snapshot, inspect
env.schema_manifest for schema descriptions and citations, or use
env.applied_schemas("physics.speed") to find schemas applied along a value’s
path. env.contributing_schema("physics.speed") identifies the schema that
supplied that value node, if any; selected value nodes also expose its
schema_id. Each record includes metadata, source_name, source_line, and
an optional source identity with its path and hash. env.schemas remains
empty after loading; the snapshot cannot instantiate schemas.
The command API also supports argument_add("schema_string", [name, body])
and argument_add("schema_file", [name, path]); file paths there are strings.
Overriding values
Use a $override region in DIPL, or register an unwrapped override body
before parse():
dip = DIP()
dip.add_override_string("simulation.steps = 1024")
# Alternatively, read an unwrapped body from a file:
# dip.add_override_file("overrides.dip")
dip.add_string("simulation\n steps int = 100")
env = dip.parse()
print(env["simulation.steps"].value) # 1024
Override bodies also accept DIPL references, expressions, and calls to
registered value functions, for example
dip.add_override_string("radius = ({?base} * 2) cm"). The replacement is
evaluated at the target declaration, so its dependencies must already be
available then. See Overriding initial values for examples, evaluation order, and
unit conversion rules.
Override bodies contain path = value modifications and optional nested
path prefixes, such as "simulation\n steps = 1024". Indentation starts
at zero in string and file bodies. Each expanded path may
appear once and must have a normal declaration. Overrides also replace
!constant values during initial evaluation. Later ordinary modifications
are ignored; dependent expressions and conditions see the replacement value.
Existing conditions may activate or deactivate nodes. A target that is not
instantiated is an unresolved override error. Registration can accompany
add_project("DIPfile"); a rejected override body registers no entries. Inspect
node.override on a selected node and cursor.provenance.override_source
for its origin. DIPH5 preserves both the effective value and override provenance.
Environment persistence
Save evaluated parameters to a DIPH5 file and restore them through
Environment:
from scinumtools3.dip import DIP, Environment
parser = DIP()
parser.add_string("simulation.steps int = 100")
env = parser.parse()
env.save("parameters.diph5")
restored = Environment()
restored.load("parameters.diph5")
assert restored["simulation.steps"].value == 100
See Environment persistence for the format, save/load behavior, and current limitations.
The command-oriented API also supports persistence. Configure the command
with argument_save() or argument_load(); the file is written or read
when execute() is called:
from scinumtools3.api.dip import DIPParse
save = DIPParse()
save.argument_add("string", ["simulation.steps int = 100"])
save.argument_save("parameters.diph5")
save.execute()
load = DIPParse()
load.argument_load("parameters.diph5")
load.argument_request("simulation.steps")
load.argument_value("integer")
assert load.execute() == "100\n"
argument_load() cannot be combined with argument_add(). Saving
overwrites the destination and always includes the full environment,
regardless of request or tag filters. Output validation must succeed before
saving. These command methods take file paths as strings; use str(path)
for a pathlib.Path.
Generating static parameters
Export an evaluated environment with Environment.generate() when a
validated DIPL configuration should become a source or data file consumed by
another application. Import ExportFormat from scinumtools3.dip and
choose one of CPP, C, FORTRAN, RUST, JULIA, JSON, or
YAML:
from scinumtools3.dip import DIP, ExportFormat
parser = DIP()
parser.add_string("simulation.steps int = 100")
env = parser.parse()
env.generate(ExportFormat.CPP, "parameters.hpp")
env.generate(ExportFormat.JSON, "parameters.json")
The command-oriented Python API can generate during execute() as well:
from scinumtools3.api.dip import DIPParse
generate = DIPParse()
generate.argument_add("file", ["parameters.dip"])
generate.argument_generate("rust", "parameters.rs")
generate.execute()
Generation always exports the complete evaluated environment; requests and tags only restrict textual output. See Static parameter generation for native representations and format-specific behavior.
Generating reports
Call Environment.generate_report() after parsing a project or loading a
DIPH5 snapshot. ReportFormat selects TeX, PDF, Markdown, reStructuredText,
HTML, Typst, plain text, or Brief++ document JSON:
from scinumtools3.dip import DIP, ReportFormat
parser = DIP()
parser.add_project("DIPfile")
env = parser.parse()
env.generate_report(ReportFormat.TEX, "report.tex",
input_label="DIPfile", intro_file="introduction.tex")
env.generate_report(ReportFormat.PDF, "report.pdf",
input_label="DIPfile", intro_file="introduction.tex",
title="Mock Heat Flow Study", author="Example Research Team",
date="2026-09-28", version="1.0 demo")
env.generate_report(ReportFormat.HTML, "report.html", input_label="DIPfile")
env.generate_report(ReportFormat.MARKDOWN, "report.md")
The remaining values are RST, TYPST, TEXT, and JSON.
Markdown uses MyST-style table directives; JSON contains a briefpp/1
document tree. These text formats need no external tools.
intro_file is an optional trusted LaTeX fragment without a preamble,
accepted only for TeX and PDF.
PDF output needs a local TeX compiler; pass tex_compiler="lualatex" or
another compatible executable when required. The same method works on an
Environment restored with load() and reports only provenance retained
in DIPH5. The default title is DIP parameter report; an empty author
appears as Not specified, while date and version default to the local
generation date and SNT build version. The PDF has a cover and linked contents
page; TeX output needs no external tool. See the CreateReport example for a PDF and Environment persistence for DIPH5 limits.
Source provenance
Each value cursor exposes a read-only provenance object. It contains the
source name, source line, captured source line, and DIPL citation metadata.
DIPH5 version 2 also preserves a source manifest, available as
environment.source_manifest, with recorded source paths and SHA-256
content fingerprints:
provenance = env["simulation.steps"].provenance
print(provenance.source_name, provenance.source_line)
print(provenance.metadata.doi)
if provenance.source is not None:
print(provenance.source.path, provenance.source.hash)
When a value was overridden, provenance.override_source,
provenance.override_line, and provenance.override_code identify the
effective value’s origin; the original declaration fields remain available.
Inspecting values and tables
The read-only DIP inspection API gives a renderer stable paths, effective
values, units, metadata, source locations, applied changes, and schema facts.
inspect_values(env) retains environment order. A value’s changes list
is also in evaluation order. inspect_capabilities(env, path) tells a client
whether a path contains a value, children, an array, or a table. Flags for
reference graphs and direct editing remain false.
from scinumtools3.dip import inspect_capabilities, inspect_table, inspect_value, read_value_slice
if inspect_capabilities(env, "measurements").has_tabular_data:
table = inspect_table(env, "measurements")
for column in table.columns: # original DIPL header order
if table.rows:
values = read_value_slice(env, column.path, [(0, table.rows - 1)])
print(column.name, column.units, values)
speed = inspect_value(env, "physics.speed")
print(speed.value, speed.declaration_location, speed.override_location)
inspect_tables(env) lists every table. Column values live at their paths;
the table inspection object contains column metadata and row count. Slice
ranges are zero-based and inclusive, and read from an evaluated in-memory
value. DIPH5 loading is still eager. open_artifact(path) loads a DIPfile,
DIPL file, or DIPH5 snapshot, while reload_artifact(env, path) replaces an
environment after a successful load.
DIP exceptions provide a diagnostic attribute containing a structured
category, message, details, suggestion, and available source locations:
from scinumtools3.dip import diagnostic_from_exception, reload_artifact
try:
reload_artifact(env, "DIPfile")
except RuntimeError as error:
diagnostic = diagnostic_from_exception(error)
if diagnostic is not None:
print(diagnostic.code, diagnostic.message, diagnostic.location)
Python values and NumPy
The C++ VAL layer maps to ordinary Python values: int, float,
str, bool, lists, and numpy.ndarray. There is no separate VAL
value class to learn, so results work directly with normal Python and NumPy
code.
Why EXS is not exposed
EXS is used internally by PUQ and DIP, but is not exposed as a standalone Python module. Python already has mature tools for custom expression evaluation and grammars; a direct EXS binding would be useful mainly when the same custom language must run across Python, C++, the CLI, and REST services.
For application-oriented operations, use the Python API helpers exposed by
the installed package. They evaluate PUQ and DIPL definitions through the
same implementation as the snt command-line tool and the C++ API. The
Python binding is therefore the preferred interface when a Python program
needs typed results or direct access to module objects without starting a
subprocess.
For installation options and the complete Python API, see the Python binding README.