leakcheck
Audit train/test leakage within and across ML benchmark datasets.
Benchmarks are routinely assembled from one another, so a "held-out" test function can already
sit in a different dataset's training split. leakcheck finds those overlaps, tells you which
ids are affected, flags records that carry conflicting labels, and writes a clean copy of a
test split with the leaked records removed.
It was written after a real finding: about a third of DiverseVul's test functions also occur in PrimeVul's training split (see Worked example).
- Inputs: JSONL / JSON / CSV / Parquet files (also globs, directories,
.gz), or Hugging Face dataset ids (offline-capable), with a text field and optional label / split / id fields. - Matching:
exact,whitespace(default),code(comment-aware), plus optional--nearnear-duplicate detection (MinHash/LSH). - Outputs: split x split and dataset x dataset overlap matrices, the overlapping ids, label-conflict cases, and a filtered test split.
- Pure standard library by default. Extras:
[parquet](pyarrow),[hf](datasets),[fast](numpy).
PyPI distribution name: dataset-leakcheck (the name leakcheck is taken on PyPI);
the import name and the CLI are both leakcheck. Not yet published from this checkout.
pip install dataset-leakcheck # core
pip install "dataset-leakcheck[parquet,hf,fast]"
# from a clone:
pip install -e ".[test]"
Quick start
# Audit two datasets. NAME:SPLIT=SOURCE puts every row of a file into that split.
leakcheck audit \
-d primevul:train=pv_train.jsonl -d primevul:test=pv_test.jsonl \
-d diversevul:test=dv_test.jsonl \
--text-field func --label-field target \
-o diversevul.label=label -o primevul.id=idx -o diversevul.id=id \
--out-dir leak_report
# Write a copy of the test split without anything that occurs in the training split.
leakcheck clean \
--test diversevul:test=dv_test.jsonl --against primevul:train=pv_train.jsonl \
--text-field func -o diversevul.id=id --out dv_test_clean.jsonl --report dropped.json
-d NAME[:SPLIT]=SOURCE is repeatable. If a file has a split column use --split-field; Hugging
Face datasets contribute their own split names. Field flags set defaults for all datasets;
-o NAME.KEY=VALUE overrides one dataset (KEY = text|label|split|id|config|hf_split).
If no id field is given the row index is the id.
What it reports
For audit (text to the terminal, plus --json FILE|- and --out-dir DIR):
| Output | Meaning |
|---|---|
| split x split matrix | [A][B]: records of group A (dataset/split) that also occur in B, with count and fraction of A. The diagonal [A][A] is records that have an internal duplicate. |
| dataset x dataset matrix | [X][Y]: records of dataset X (any split) that occur anywhere in dataset Y. The diagonal is X's records that appear in a different split of X (within-dataset leakage). |
| overlapping ids | one row per record, from -> to: its id, the matching ids on the other side (up to 5), exact or near, similarity. Full list in overlaps.csv. |
| label conflicts | same (normalised) text carrying different labels, within-group or cross-group. Labels compare as True == 1 == "1". Exact-key matches only. Full list in label_conflicts.jsonl. |
Records whose text is empty after normalisation never match anything and are counted as empty.
--fail-on-overlap makes audit exit 1 if any cross-group overlap exists (useful in CI).
clean keeps the original rows of the test split (all columns, unchanged), drops every record
that matches anything in --against, and prints how many were dropped. Options: --dedup (also drop
repeats inside the test split, keep first), --drop-conflicts (also drop test records whose text
appears with a different label). --out may be .jsonl, .csv or .parquet; --report records
every dropped id and the reason.
Normalisers and matching
--normalizer |
Two texts are "the same" when ... |
|---|---|
exact |
byte-identical |
whitespace (default) |
identical after removing all whitespace ("".join(text.split()), the rule used in the original experiments) |
code |
identical after stripping comments, then all whitespace. --lang c (default; // and /* */: C, C++, Java, JS, Go, Rust, ...), python (#), or auto (heuristic). String literals are respected. Not handled: raw/verbatim strings, the preprocessor; Python docstrings are kept as strings. |
--max-chars N truncates each text to N characters before normalising, to compare exactly the
view a model was trained on (e.g. --max-chars 4000).
Near-duplicates (--near)
Two texts are near-duplicates when the **Jaccard similarity of their sets of 5-token shingles is
= 0.8** (
--shingle 5,--threshold 0.8). Tokens are words and punctuation after comment stripping (--lang). MinHash (128 permutations) with banded LSH is used only to find candidates cheaply; every candidate is verified with the exact Jaccard, so reported similarities are exact and nothing below the threshold is reported. The band layout follows the threshold (16x8 at 0.8: about 95% recall at J=0.8, ~100% at J>=0.9; pairs far above the threshold are essentially never missed). Pairs near the threshold can be missed, so treat--nearas a lower bound on near-duplication. Exact-matching records are collapsed first so large duplicate groups stay cheap. Near matches count in the matrices, the id list andclean; label-conflict detection stays exact-key only. numpy, if installed, speeds up signatures (results are identical to the pure-Python path).
Library API
import leakcheck as L
src = [
L.Source("primevul", "pv_train.jsonl", split="train", text_field="func", id_field="idx"),
L.Source("diversevul", "dv_test.jsonl", split="test", text_field="func", id_field="id",
label_field="label"),
]
res = L.audit(src, L.Options(normalizer="code", near=True))
res.split_matrix["diversevul/test"]["primevul/train"] # {"count": ..., "fraction": ...}
res.overlaps # [{"from", "id", "to", "match_ids", "match", "similarity", ...}]
res.label_conflicts
out = L.clean(src[1], [src[0]], L.Options(normalizer="code"), dedup=True)
out.kept # list[Record]; r.raw is the original row
out.dropped # [{"id", "reason", ...}]
Also: audit_records / clean_records for rows you already hold as Records, LeakIndex for custom
queries, and the normalisers in leakcheck.normalize.
Hugging Face ids and offline use
-d prime=hf:colin/PrimeVul loads through the optional datasets package, one group per HF split
(or just hf_split via -o prime.hf_split=train). leakcheck never downloads by itself beyond what
datasets does; set HF_HUB_OFFLINE=1 to force the local cache. Locally cached parquet snapshots can
also be passed as a glob, e.g. ~/.cache/huggingface/hub/datasets--bstee615--diversevul/snapshots/*/data/test-*.parquet.
Worked example: PrimeVul -> DiverseVul
Commands only; no dataset contents are included in this repository. Obtain both datasets under
their own licences (for example into the Hugging Face cache) and set HF_HUB_OFFLINE=1 if offline.
pip install "dataset-leakcheck[hf,parquet,fast]"
# 1. Cross-dataset audit: how much of DiverseVul's test split sits in PrimeVul's splits?
# PrimeVul columns: func, target. DiverseVul columns: func, target.
# --max-chars 4000 reproduces the 4000-character view the original run trained on.
leakcheck audit \
-d primevul=hf:colin/PrimeVul \
-d diversevul=hf:bstee615/diversevul -o diversevul.hf_split=test \
--text-field func --label-field target \
--normalizer whitespace --max-chars 4000 \
--out-dir leak_primevul_diversevul
# Read: dataset x dataset [diversevul][primevul] -> fraction of DiverseVul test records
# that also occur in PrimeVul; split x split [diversevul/test][primevul/train] -> the
# train-overlap that invalidates a "train on PrimeVul, test on DiverseVul" claim.
# label_conflicts.jsonl lists texts the two datasets label differently.
# 2. A clean DiverseVul test split: remove everything present in PrimeVul train.
leakcheck clean \
--test diversevul=hf:bstee615/diversevul -o diversevul.hf_split=test \
--against primevul=hf:colin/PrimeVul -o primevul.hf_split=train \
--text-field func --label-field target \
--normalizer whitespace --max-chars 4000 \
--out diversevul_test_clean.parquet --report dropped.json
# 3. Stricter variants: ignore comment edits, and also catch near-duplicates.
leakcheck audit -d primevul=hf:colin/PrimeVul -d diversevul=hf:bstee615/diversevul \
--text-field func --normalizer code --near --threshold 0.8 --json -
The motivating finding (whitespace-normalised exact match, 4000-char truncation, DiverseVul test
sampled against all of PrimeVul train) was roughly one third of the test functions; re-scoring a model
on the strict-clean subset is the point of clean. Your numbers depend on the dataset revisions you
use, so run the audit rather than quoting this one.
Development
pip install -e ".[test]" && pytest # synthetic fixtures only, no network
python -m build # sdist + wheel
License
MIT. Copyright (c) 2026 Pallav Pokhriyal.
Input validation (0.2.0)
Bad input is an error, never a silent success. Every failure is {code, message, hint} (--json) with exit 2:
LC_E_EMPTY_INPUT (empty, whitespace-only, comment-only or header-only file, []), LC_E_BAD_VALUE
(text that is not a string or null), LC_E_MISSING_FIELD (--text-field/--id-field absent from a row, or
--label-field/--split-field present in no row; -o NAME.label= with an empty value switches it off for one
dataset), LC_E_BAD_SPEC (-o naming a dataset that was not given), LC_E_BAD_FORMAT (rows that are not
objects, corrupt/fake .gz, bad parquet, invalid JSON/UTF-8), LC_E_IO (unreadable file), LC_E_USAGE
(--shingle/--max-chars <= 0, --threshold outside (0, 1], negative --max-rows, unknown or abbreviated
options). A UTF-8 BOM is accepted. A null text is kept and counted as empty.
Metadata
Release files for dataset-leakcheck 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dataset_leakcheck-0.2.0.tar.gz | 44.8 kB | Without provenance | Details for dataset_leakcheck-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dataset_leakcheck-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for dataset_leakcheck-0.2.0-py3-none-any.whl |
Total release size: 84.8 kB
Release files / dataset_leakcheck-0.2.0.tar.gz
| Download URL | dataset_leakcheck-0.2.0.tar.gz |
|---|---|
| Size | 44.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
75876146aef0276aa9f77c081f424d75ea5ff68c22264f8cd27543fc4c81bcbb
|
|
BLAKE2b-256 checksum How to use checksums |
5671871200fdb51bd282aaf54d5ff2f205093f9bb4ea775c25db5cd046c51666
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / dataset_leakcheck-0.2.0-py3-none-any.whl
| Download URL | dataset_leakcheck-0.2.0-py3-none-any.whl |
|---|---|
| Size | 40.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
22cd31553b939a9218800c20cea0ae2d5112621ac53191b1a1c4a4739bba461a
|
|
BLAKE2b-256 checksum How to use checksums |
23cfb46aa9e516fb50fd5e5adb170626bef5de42eb00d85e565272b40f211a5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for dataset-leakcheck 0.2.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.