Skip to main content

Spynach

A Python reimplementation of the Spinach spin dynamics kernel (MIT licence, see LICENSE), verified stage by stage against the MATLAB original. Start with the Hilbert-space guide; if you use Spinach, also read Coming from Spinach. Each guide has a runnable Jupyter notebook in docs/notebooks/. The project page at ajerschow.github.io/spynach links the guides, the notebooks (rendered by nbviewer) and the Spinach comparison reports, and has an "Open in Google Colab" button that runs the quick-start example without a local install (docs/colab/quick-start.ipynb), and a page that runs Spynach, easyspyn and ctps in the browser through Pyodide (try.html, contributed by Moritz Zaiss). See HANDOFF.md for goals and design decisions, and oracle/README.md for the reference-data ("fixture") harness.

Quick start

Nothing to install: run it in the browser at ajerschow.github.io/spynach/try.html (Pyodide; NMR, EPR and pathway examples you can edit), or open a notebook in the cloud: Open in Colab: EPR Open in Colab: NMR Launch Binder

Install (Python 3.11 or newer):

pip install spynach    # NMR: Spinach's kernel, from PyPI (installs Numba; also provides ctps)
pip install easyspyn   # EPR: EasySpin's simulators, from PyPI (its own distribution, installs Numba)
pip install "spynach[epr]"   # both
import easyspyn as es

nitroxide = {"g": [2.0088, 2.0061, 2.0027], "Nucs": "14N", "A": [16, 16, 86], "lwpp": 0.3}
powder = es.pepper(nitroxide, {"mwFreq": 9.5, "Range": [330, 350], "nPoints": 1024})
print(powder.x.shape, powder.spec.shape)

Status

Ported and verified against Spinach 2.13 fixtures (tests/slice/): the zeeman-hilb pulse-acquire chain for liquid-state NMR with isotropic interactions. The sphten-liouv Liouville-space formalism (basis with state-space restriction, superoperators, states, Hamiltonian and kinetics superoperators, analytical filters, propagation with zero track elimination, path tracing and destination state screening) runs pulse-acquire and chemical exchange end to end; its setup is verified against Spinach 2.13 fixtures (tests/slice/test_liouv_setup_parity.py) and its propagation against Spinach 2.13 fixtures as well (tests/slice/test_liouv_pa_parity.py), Hilbert space, and analytic exchange line shapes. Anisotropic interactions (shielding and coupling tensors, dipolar couplings from coordinates, quadrupolar couplings) with the crystal and powder contexts are ported and verified against Spinach 2.13 fixtures as well (tests/slice/test_aniso_parity.py). Relaxation (Redfield theory, T1/T2 and damping models, thermalisation) is ported and verified against Spinach 2.13 fixtures (tests/slice/test_relax_parity.py) and the Solomon equations; scalar relaxation of the first and second kind (SRFK, SRSK) is ported and verified against Spinach 2.13 fixtures (tests/slice/test_scalar_parity.py), Abragam's expressions and the explicit integral. Magic angle spinning in Fokker-Planck form (singlerot) is ported and verified against Spinach 2.13 fixtures (tests/slice/test_mas_parity.py), rotor echoes and the fast-spinning limit. The pulse sequence library (COSY, NOESY, HSQC with isotopomer dilution, analytical decoupling, apodisation and 2D plotting) is ported and verified against Spinach 2.13 fixtures (tests/slice/test_seq_parity.py).

import numpy as np
import spynach

spin_system = spynach.create(
    {"isotopes": ["1H", "1H"], "magnet": 14.1},
    {"zeeman": {"scalar": [1.00, 1.10]}, "coupling": {"scalar": [[0, 12.0], [0, 0]]}},
)
spin_system = spynach.basis(spin_system, {"formalism": "zeeman-hilb", "approximation": "none"})
pars = {
    "spins": ["1H"],
    "offset": 630,
    "sweep": 200,
    "npoints": 1024,
    "rho0": spynach.state(spin_system, "L+", "1H"),
    "coil": spynach.state(spin_system, "L+", "1H"),
}
fid = spynach.liquid(spin_system, spynach.acquire, pars, "nmr")
fid = spynach.apodisation(fid, [("exp", 6)])  # halve the first point, exponential window
spectrum = np.fft.fftshift(np.fft.fft(fid, 4096))
Spinach Spynach ported
create.m, dipolar.m spynach.create isotopes, magnet, tolerances, Zeeman and coupling tensors as scalars, matrices, or eigenvalues with Euler angles, coordinates with point dipolar couplings and the proximity graph, chemical subsystems (inter.chem parts, concentrations, exchange rates)
basis.m spynach.basis zeeman-hilb; sphten-liouv with none, IK-0, IK-1, IK-2, the projections/longitudinal/zero_quantum/manual filters, and chemical subsystems (no symmetry, no coordinates)
operator.m, state.m spynach.operator, spynach.state Hilbert space and sphten-liouv superoperators (left, right, comm, acomm); states by the exact, cheap and chem methods; E, Lx, Ly, Lz, L+, L-, Tl,m
assume.m spynach.assume nmr, esr, deer, deer-zz, labframe, with zeeman/couplings retention
hamiltonian.m, orientation.m, frqoffset.m spynach.hamiltonian, spynach.orientation, spynach.frqoffset isotropic part and the rank-1/rank-2 irreducible components Q (CSA, anisotropic couplings, quadrupolar); in sphten-liouv as left/right/comm/acomm superoperators
euler2dcm.m, mat2sphten.m, wigner.m, eeqq2nqi.m spynach.conventions the same conventions
kinetics.m, equilibrate.m spynach.kinetics, spynach.equilibrate first-order exchange between chemical subsystems; equilibrium concentrations
trajan.m, stateinfo.m spynach.trajan, spynach.stateinfo, spynach.plotting.plot_trajan correlation-order, coherence-order and per-spin amplitudes along a trajectory; the largest coefficients of a state (no level_populations)
correlation.m, coherence.m spynach.correlation, spynach.coherence sphten-liouv
propagator.m, step.m, krylov.m spynach.propagator, spynach.step, spynach.krylov dense Pade below small_matrix, scaled-squared Taylor above; Taylor action on vectors with 1-, 2-, 3-point Lie quadratures
evolution.m spynach.evolution zeeman-hilb: observable; sphten-liouv: observable, final, trajectory, multichannel, refocus, destination state screening, Krylov path
reduce.m, zte.m, path_trace.m spynach.reduce, spynach.zte, spynach.path_trace zero track elimination, path tracing with subspace merging; no symmetry
relaxation.m, equilibrium.m, thermalize.m spynach.relaxation, spynach.equilibrium, spynach.thermalize Redfield (isotropic, axial, rhombic diffusion; diagonalisation-free integral), t1_t2, damp, SRFK (rlx_scalar.m), SRSK; rlx_keep, rlx_dfs; zero/IME/dibari thermalisation
liquid.m, crystal.m, powder.m, singlerot.m, fourdif.m, acquire.m spynach.liquid, spynach.crystal, spynach.powder, spynach.singlerot, spynach.contexts.fourdif, spynach.acquire both formalisms (MAS in Liouville space); relaxation and exchange kinetics; powder averages over Spinach's grids (shipped as spynach/grids); MAS in Fokker-Planck form with the Fourier spectral derivative; no decoupling
ft_axis.m, axis_1d.m, plot_1d.m, plot_2d.m, contspacing.m, apodisation.m spynach.plotting, spynach.apodisation all axis units; interactive Plotly 1D and 2D contour figures; window functions except bad-z*; no derivative
etc/molecules/strychnine.m spynach.molecules.strychnine the 22-proton system (shifts, couplings, coordinates); 13C/15N not ported
cosy.m, noesy.m, hsqc.m, decouple.m, homospoil.m, dilute.m, kill_spin.m spynach.cosy, spynach.noesy, spynach.hsqc, spynach.decouple, spynach.homospoil, spynach.dilute, spynach.kill_spin COSY, States-TPPI NOESY, echo-antiecho HSQC; analytical decoupling; isotopomer sums

Anything not ported raises NotImplementedError; inputs are never silently ignored.

Differences from Spinach

  • Spin indices are 0-based (operator(spin_system, ["Lz", "L+"], [0, 1]) is Spinach's operator(spin_system, {'Lz','L+'}, {1,2})). Isotope names and 'all'/'nuclei'/'electrons' work as in Spinach.
  • Product vs. sum operators are distinguished by the type of the first argument: a string ("Lz") gives a sum over spins, a list (["Lz", "L+"]) a product operator.
  • Spin system objects are frozen dataclasses; basis() and assume() return modified copies, as Spinach returns a modified struct.
  • Time series are 1-D arrays (MATLAB returns n x 1 columns).
  • evolution() runs serially; Spinach's Kuprov-Edwards parallel split computes the same observable.

Beyond the port

Add-ons originating here, with no Spinach counterpart, each with an example in the guide Beyond the port:

  • ctps, a separate module (import ctps, no dependency on spynach) that ports CCCP.m and CCCP++, the coherence transfer pathway programs: every pathway a sequence allows, weighted by phase cycle, gradients, diffusion and pulses, with an interactive pathway explorer (docs/ctps.md, src/ctps/README.md for how to remove it without touching Spynach).
  • Non-ideal pulses (perfect 180°, imperfect 180°, small flip angle) after J. Chem. Phys. 113, 979 (2000), and a cogwheel cycle search by exact integer arithmetic (find_cycle, coverage, hcl_cycle) plus the DOTCOPS crusher objective (crusher_scheme), in ctps.
  • Pulse programs, gradients, phase cycling (beta) (spynach.program): gradients without a spatial grid. A sequence written as blocks with its phase lists and gradients runs as one branch of the density matrix per coherence pathway that survives the phase cycle (pathways and weights from ctps), each branch carrying its gradient k-vector and diffusion integral, so gradient selection, crushers, read-out gradients and PGSE diffusion are exact per-pathway factors with no voxels and no Nyquist limit; per-pathway FIDs and echo times, and the explicit cycle as the reference. The phase-cycle and gradient selection are new and in beta: the API may still change (docs/pulse-programs.md; browser examples ex-dqf-cosy, ex-phase-cycle, ex-gcosy, ex-pathway-echoes, ex-pgse, and a 23Na triple-quantum filter by phase cycling and by gradients, checked against each other: ex-na23-tqf-cycle, ex-na23-tqf-gradients).
  • Relaxation pathways by mechanism (spynach.relaxation_pathways): the Redfield relaxation matrix between product operators split into dipolar, CSA, quadrupolar and anisotropic-J contributions and all their cross-correlations, each with an exact expression in the coupling strengths and spectral densities, drawn as a chord diagram by plot_pathways (docs/relaxation-pathways.md), also in the browser playground (try.html#ex-pathways).
  • Radiation damping (spynach.radiation_damping, rd_rate, rd_phase): the feedback of the probe on any spin system as a state-dependent generator, with isochromat ensembles for inhomogeneous lines, propagated by iserstep (a port of Spinach's Lie-group and Runge-Kutta-Munthe-Kaas integrators) and checked against the Bloembergen-Pound closed form and the classical Bloch-Maxwell limit (docs/radiation-damping.md), also in the browser playground (try.html#ex-rd).
  • Interactive Plotly figures from spynach.plotting, plot_fid, plot_time_series and plot_trajan with no Spinach equivalent, and figure_png for static copies.
  • Credits: the Spinach authors of every translated function, by name, in its docstring (help(spynach.propagator)) and in docs/spinach-credits.md; for EasySpyn, the EasySpin contributors to each original file (docs/easyspin-credits.md).
  • Verification tooling: spynach.oracle.compare for checking any result against a Spinach run, the generated parity page docs/parity.md, and the timed, profiled comparison protocol in comparison/.
  • Executable documentation: guides whose code runs in the tests, notebooks generated with outputs, a Colab quick start, and contributed notebooks in user_py/.
  • Browser-based execution: try.html runs Spynach, easyspyn and ctps in the browser through Pyodide, with nothing installed, and plots spectra, FIDs and 2D contours from an editor preloaded with NMR, EPR and pathway examples (contributed by Moritz Zaiss). Its Parameters view (a "Prefer GUI" / "Prefer code" toggle next to Run) shows the main settings as a form with units and descriptions, kept in step with the code.
  • easyspyn, a separate package (import easyspyn, depends on spynach, never the reverse) that ports EasySpin to Python with EasySpin's interface: the foundation (spin systems, sop, stev, ham with the field split, levels, rotations, units, nuclear data), resfields and the line shapes, the cw simulators pepper (powders and crystals) and garlic (solutions), least-squares fitting with esfit, ENDOR with salt, magnetometry with curry, slow-motion spectra with chili and pulse EPR (ESEEM, HYSCORE, Mims ENDOR) with saffron, verified against EasySpin 6.0.15 (docs/easyspyn.md, docs/coming-from-easyspin.md, src/easyspyn/README.md for how to remove it without touching Spynach).

Accuracy notes

Every parity comparison is relative with zero absolute tolerance, at 1e-14 for operators and states, 1e-12 for Hamiltonians and 1e-9 for anything through a propagator or an FID; docs/parity.md, regenerated from a run of the suite by oracle/parity_summary.py, lists every fixture case, tolerance and measured difference. To check a workflow of your own against a Spinach run, see the section Checking your own workflow against Spinach.

  • Spin operators match Spinach to 1 ulp: MATLAB evaluates 2^(-1/2) one ulp below the correctly rounded value, so Spinach's Lz is 0.4999999999999999.
  • Spinach rounds Hamiltonian elements to a 1e-10 rad/s grid (clean_up) after summing terms in random order, so Spinach itself reproduces only to about one grid step per element; FIDs therefore agree to ~1e-10 x acquisition time.
  • The Taylor propagator's error scales with tols.prop_chop and the number of squarings: about 7e-8 relative at the default 1e-10 with 7 squarings.

Development

From a clone of the repository, an editable install with the test and notebook tools runs the test suite and the guides' notebooks (jupyter lab docs/notebooks/):

python -m pip install -e ".[test,docs]"
python -m pytest -q

Without the install, PYTHONPATH=src python -m pytest -q runs the tests from the source tree.

Parity tests skip when fixtures are absent ($SPYNACH_FIXTURES, default /fixtures). A caveat for containers: when the CPU quota is below the visible core count (the development container sees 12 cores under a 4-CPU quota), OpenBLAS oversubscribes the quota and dense LAPACK calls run ~1000x slower (a 256x256 eigh takes 10 s instead of 10 ms). Set OPENBLAS_NUM_THREADS to the quota there, for tests and especially for benchmarks; on an ordinary machine nothing needs to be set.

To test on a macOS host outside the container, install uv (brew install uv) and run scripts/mac-test.sh (extra arguments go to pytest). The container's .venv does not work on the host, so the script keeps a separate .venv-mac and syncs it with pyproject.toml before every run. Source edits need no reinstall. Point VS Code's interpreter at .venv-mac/bin/python. scripts/mac-compare.sh runs the Spynach side of a comparison case in the same environment (with the compare extra), so that both sides of a report are timed on the same machine.

Releases are tagged vX.Y.Z and listed in CHANGELOG.md; spynach.__version__ gives the installed version, and pip install spynach==0.7.0 installs a tagged one from PyPI (before 0.5.0: pip install "spynach @ git+https://github.com/ajerschow/spynach.git@v0.3.0"). To release: set the version in pyproject.toml, src/spynach/__init__.py and CITATION.cff (a test keeps them equal), add the CHANGELOG.md entry, refresh uv.lock, run the checks above plus python docs/build_notebooks.py --check and python oracle/parity_summary.py --check, commit, tag with git tag -a vX.Y.Z -m "Spynach X.Y.Z", push with --follow-tags, and draft the GitHub release from the tag with the changelog entry as its notes. Publishing that release runs .github/workflows/publish-spynach.yml, which tests and builds the commit and, once the reviewer of the pypi environment approves, uploads it to PyPI as spynach (Trusted Publishing, no token; the tag must equal the version in pyproject.toml). The distribution holds spynach and ctps; the epr extra pins the matching easyspyn, so release that first (easyspyn-vX.Y.Z, see src/easyspyn/README.md). A version can be uploaded only once.

Code style is enforced by ruff (pip install -e ".[dev]"), configured in pyproject.toml: 120-character lines, import sorting, pyupgrade and bugbear checks. Before committing, run

ruff format && ruff check

Spinach's own matrix names (I, Q, L, A, B, ...) are kept where a function mirrors a Spinach function, so the ambiguous-name check is off.

Every user-facing change comes with user documentation in the same commit; see docs/README.md for the conventions. After editing a guide, regenerate its notebook with python docs/build_notebooks.py; tests/docs/ fails if it is stale.

The project site is hand-written HTML at the repository root, served by GitHub Pages from the root of main (Settings > Pages > Deploy from a branch > main, / (root); .nojekyll keeps Pages from preprocessing the tree): index.html (landing page), spinach.html, easyspin.html, extras.html, docs.html, and try.html, the playground that runs Spynach, easyspyn and ctps in the browser (worker.js loads Pyodide in a web worker and fetches the sources). They share assets/site.css. The parts that are data rather than prose sit between <!-- site:... --> markers and are written by scripts/build_site.py: the navigation, the guide tables of docs.html (from the table in docs/README.md, filed by package in the script's groups), the timing tables of spinach.html (from comparison/runs/*/summary.json, which comparison/report.py writes beside each report) and easyspin.html (from comparison/easyspin/timings.json, written by scripts/easyspin-timings.py from the EasySpin fixtures), and the SOURCE list of worker.js; tests/unit/test_site.py fails when a span is stale. The parity paragraph of spinach.html comes from oracle/parity_summary.py. Reports are linked relatively, so they are served by Pages; guides and notebooks link to GitHub and nbviewer. The playground's examples are written into try.html and run natively by tests/unit/test_playground_examples.py and through the worker's Python runner by tests/unit/test_playground_runner.py; scripts/pyodide-examples.mjs runs them in Pyodide itself under Node (npm install pyodide in a scratch directory), which is the only way to catch what differs in 32-bit WebAssembly: NumPy's index type is int32 there, so integer arrays used as repeat counts or bincount indices must be np.intp, never np.int64. The Parameters view reads # GUI: label | unit | description | range comments on the assignments at the top of an example (assets/playground-params.js, documented in its header); tests/unit/test_playground_params.py checks that each parameter is documented and that the example runs with it at both ends of its range. The hero figure is assets/Spynoza.jpg; assets/liouvillian-strychnine*.png are sparsity patterns of the strychnine NOESY Liouvillian rendered by scripts/landing-figure.py (needs Pillow and Matplotlib, which the compare extra brings in), kept as alternatives.

src/spynach/_isotope_data.py is generated from Spinach's spin.m by porting/tools/gen_isotopes.py; do not edit it by hand. src/spynach/grids/*.npz are generated from Spinach's kernel/grids by porting/tools/gen_grids.py. examples/ holds full-size translations of Spinach example scripts for side-by-side comparison with MATLAB (see docs/README.md); comparison/ holds the protocol for timed, profiled Spinach-vs-spynach runs with an HTML report (comparison/README.md). porting/tools/bench_strychnine.py times the kernel on the 22-proton strychnine system (12 316 states at IK-2, proximity level 3): basis 0.04 s, Hamiltonian 0.1 s, Redfield relaxation 2.4 s, pulse-acquire 2.1 s, COSY 32 x 64 3.3 s in this container.

Contributors

  • Florin Teleanu: Spinach scripts in user_ml/ (two-site 7Li exchange at equilibrium, zero- to ultralow-field PF6⁻ spectrum), translated in examples/user_ml/ and used as comparison cases, and the project page's hero figure (assets/Spynoza.jpg); and the relaxation pathways module (spynach.relaxation_pathways, docs/relaxation-pathways.md, the try.html#ex-pathways example; pull request #2).
  • Moritz Zaiss (MRsources): the in-browser playground (try.html, worker.js, Spynach in Pyodide) and the orientation progress callback of powder and singlerot (pull request #1).
  • The port was a collaboration between Claude (Anthropic) and Alexej Jerschow, who also contributed several new scripts and functions (for example the port of the coherence pathway calculations, ctps).

New contributors are added here and in the "With thanks" section of index.html when their first contribution is merged; the CHANGELOG names them in the release entry.

Licence and attribution

Spynach is released under the MIT licence (LICENSE). It is derived from Spinach by Ilya Kuprov and co-workers, also MIT licensed: the kernel, molecule data, isotope table, spherical grids and example scripts were translated from it, and Spinach's copyright and permission notice is reproduced in LICENSE. Keep that notice with any copy or substantial portion of this software.

The translation from MATLAB to Python was carried out largely by an AI coding agent (Claude, Anthropic) under the direction of Alexej Jerschow, who reviewed the results; commits made by the agent are marked as such in the git history. Correctness rests on the test suite's comparison with Spinach reference data, not on the method of authorship (see LICENSE).

Results obtained with Spynach should cite Spinach: H. J. Hogben, M. Krzystyniak, G. T. P. Charnock, P. J. Hore, I. Kuprov, Spinach: a software library for simulation of spin dynamics in large spin systems, J. Magn. Reson. 208, 179 (2011), together with the method papers that the Spinach documentation lists for the features used (restricted state spaces, relaxation theories, Fokker-Planck contexts).

Results obtained with EasySpyn should credit the EasySpin development effort: cite the papers on the EasySpin publication list, at least S. Stoll, A. Schweiger, EasySpin, a comprehensive software package for spectral simulation and analysis in EPR, J. Magn. Reson. 178, 42 (2006), and for pulse EPR S. Stoll, R. D. Britt, Phys. Chem. Chem. Phys. 11, 6614 (2009).

The Spynach and EasySpyn ports should be attributed to this repository, https://github.com/ajerschow/spynach (CITATION.cff gives the citation).

The ctps package is a Python port of earlier MATLAB (CCCP.m) and C++ (CCCP++) implementations of coherence pathway selection and is included in the spynach distribution. Results obtained with it should cite A. Jerschow, N. Müller, J. Magn. Reson. 134, 17 (1998); A. Jerschow, R. Kumar, J. Magn. Reson. 160, 59 (2003); for non-ideal pulses A. Jerschow, J. Chem. Phys. 113, 979 (2000); and this repository.

Scripts contributed by users in user_ml/ (Spinach) and user_py/ (Python notebooks and scripts) remain the copyright of their authors and are redistributed, with their agreement, under the same MIT licence (see the README in each directory).

Metadata

Release files for spynach 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spynach 0.7.0
File Size Uploaded Provenance Actions
spynach-0.7.0.tar.gz 5.2 MB With provenance Details for spynach-0.7.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for spynach 0.7.0
File Interpreter ABI Platform Actions
spynach-0.7.0-py3-none-any.whl Python 3 none any Details for spynach-0.7.0-py3-none-any.whl

Total release size: 10.5 MB

Release files / spynach-0.7.0.tar.gz

Download URL spynach-0.7.0.tar.gz
Size 5.2 MB
Tags Source
SHA-256 checksum
How to use checksums
4ac18fcda586d2b8e063dfe14acd9e039f6bf650285b877a80be42d436acf22e
BLAKE2b-256 checksum
How to use checksums
218bb86f1bf12ab8316263b0edbf5fbb11ec9755784d5d10335d044c6ea01b50
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / spynach-0.7.0-py3-none-any.whl

Download URL spynach-0.7.0-py3-none-any.whl
Size 5.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
066012271059bd18d20f901a14dbe0e1b39cb40506c2b6dde9996ef9c7e757b9
BLAKE2b-256 checksum
How to use checksums
dee0f0798d3d4702bf8799dcd7538de0f907bf583fb0ddce58581a60ca6c2a53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Security for spynach 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for spynach-0.7.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page