runprov
Records what a Python script actually read, wrote and ran — and writes that record into the result file itself.
A hand-maintained log describes what an author believed they did. It cannot describe what the
program did. runprov records the reads and writes as they happen, and writes the record in
two forms that need no software to read: a YAML log whose digests are in sha256sum's own
format, and a comment header inside the artifact.
One command, runprov verify, re-derives every recorded digest and reports whether a result
still follows from the inputs it was made from — or has itself been edited — reading only
the file. No database, no history, no network.
- Zero runtime dependencies. Python 3.10+.
- Observed, not declared. A CPython audit hook sees every
open, including those made by a library nobody thought to instrument; unregistered reads are recorded rather than ignored. - The record does not depend on the tool that wrote it. With the package uninstalled,
catreads the history,grepfinds every run that touched a file, andsha256sum -cverifies the digests.
pip install runprov
Adoption is one call
run.input(p) returns the path it was given, so it is written on the way to the open
that was already there:
from runprov import Run, configure
configure(root=".")
with Run("summarise", {"threshold": 5}) as run:
with open(run.input("data/measurements.tsv"), encoding="utf-8") as fh:
rows = fh.read().splitlines()
with run.open_output("results/summary.tsv") as out:
out.write(f"n\t{len(rows)}\n")
run.note("rows_read", len(rows))
The artifact then carries its own provenance, as a comment block above its first line:
# provenance — this artifact and what produced it
# script : summarise
# commit : b02d309
# body : d8c46f515971fb75
# inputs (1), content digest:
# 17ffe7054ecf0cc8 data/measurements.tsv
Inside the script, and watching it run
@run.step # digests what a function received and returned
def normalise(rows, factor=1.0):
return [r * factor for r in rows]
A digest says a file changed; this says an argument changed. Values that cannot be
canonically serialised are recorded as UNDIGESTIBLE:<type> — never a repr, never a
pickle.
On Python 3.12+ it also records which of your own functions ran, and how often, with no
decorator: sys.monitoring, scoped to code under the project root, and bounded — the
observer stops itself after 50 000 calls and the record says when. configure( auto_steps="arguments") additionally digests the distinct argument sets each function saw.
Every record carries an observation block naming what the run was able to observe, so a
record with no steps is distinguishable from one made where steps could not be observed.
[00:00] read data/m1.tsv 1541e29a8301ba21
[00:02] still running — last: read data/m1.tsv
[00:03] wrote out.tsv c533232884b32c60
Progress is on when stderr is a terminal and off otherwise; the heartbeat fires only after
silence, so a run producing events steadily never beats. Both are configured once, never per
script. run.open_output(p, record_header=True) records the column names it just wrote.
Checking it later
$ runprov verify results/summary.tsv
OK results/summary.tsv
# after one line is appended to the input:
$ runprov verify results/summary.tsv
STALE results/summary.tsv
STALE data/measurements.tsv
(pinned 17ffe7054ecf0cc8, now a3d2c39b016f6a7e)
OK, STALE, GONE and ALTERED are the four verdicts. Also available:
runprov log (the run history as YAML or a table), runprov show (what one run did),
runprov capture (record a script that has no runprov calls in it at all), and
runprov export (RO-Crate and W3C PROV-JSON).
What it is not
It records; it does not audit — it cannot tell you that a registered read was the read that
mattered. @run.step digests what crossed a function's boundary — arguments in, result out
— and not the dataflow between statements, which is what noWorkflow reports and what
requires rewriting the AST, at the cost of changing the program it observes.
It identifies versions; it does not store them. A digest says an input changed, and says
which version a result was built from — it cannot give those bytes back. That is DVC's job,
and the two compose: DVC stores content, runprov records which content a run actually read.
Provenance written into a scientific artifact is not new either: SAM/BAM @PG has carried
program, version and command line for over a decade. What is narrower and true here is a
format-agnostic pin carrying the content digest of every input, plus a checker that re-derives
them from the artifact alone.
Documentation
The full documentation is one long README in the repository — the complete API, every supported format, the failure each design decision came from, and what has actually been run:
https://github.com/Taylor-Nicole/runprov
| Why it exists, and how it compares | WHY.md |
| Design decisions | docs/adr/ |
| Changes | CHANGELOG.md |
| Security policy | SECURITY.md |
Licence
BSD 3-Clause. Copyright (c) 2026 Assistance Publique – Hôpitaux de Paris (AP-HP), Hôpital Henri-Mondor, and Taylor Thompson.
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 runprov-0.2.0.tar.gz.
File metadata
- Download URL: runprov-0.2.0.tar.gz
- Upload date:
- Size: 696.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d92e09da07f06bcd528bed761e0612ea7c4ca432418b266ee255334e067cbb1
|
|
| MD5 |
72de24e6f219ee9d6ef92b29703b6c43
|
|
| BLAKE2b-256 |
2b9750dea25ea54f443387dd423430f092a7387d82844f6171c8acf48421d9d5
|
Provenance
The following attestation bundles were made for runprov-0.2.0.tar.gz:
Publisher:
publish.yml on Taylor-Nicole/runprov
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
runprov-0.2.0.tar.gz -
Subject digest:
2d92e09da07f06bcd528bed761e0612ea7c4ca432418b266ee255334e067cbb1 - Sigstore transparency entry: 2842156224
- Sigstore integration time:
-
Permalink:
Taylor-Nicole/runprov@354a365d4cf65bd0f6b8a02f1fc9083c499036a4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Taylor-Nicole
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@354a365d4cf65bd0f6b8a02f1fc9083c499036a4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file runprov-0.2.0-py3-none-any.whl.
File metadata
- Download URL: runprov-0.2.0-py3-none-any.whl
- Upload date:
- Size: 238.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5694737d11a10e4722b976f81944d64f43ad3c5da4d5057e08e0a107b288b80c
|
|
| MD5 |
8aab7a34d434ccce846d358ba9253f40
|
|
| BLAKE2b-256 |
94da35a85a5db4060271f29075556711816673cfaab0d6fef86536ec36de0972
|
Provenance
The following attestation bundles were made for runprov-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on Taylor-Nicole/runprov
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
runprov-0.2.0-py3-none-any.whl -
Subject digest:
5694737d11a10e4722b976f81944d64f43ad3c5da4d5057e08e0a107b288b80c - Sigstore transparency entry: 2842156261
- Sigstore integration time:
-
Permalink:
Taylor-Nicole/runprov@354a365d4cf65bd0f6b8a02f1fc9083c499036a4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Taylor-Nicole
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@354a365d4cf65bd0f6b8a02f1fc9083c499036a4 -
Trigger Event:
push
-
Statement type: