Zero-mandatory-dependency temporal data-leakage linter and point-in-time guard for Python/Jupyter time-series, ML, forecasting, and backtests.
Most leakage tools ask whether train and test rows overlap. NoFutureData asks a more operational question:
At the instant this prediction or decision was made, could the pipeline have actually known every input it used?
It separates event time from availability time, statically flags common future-looking pandas patterns, and can test a feature pipeline by deleting or mutating future inputs and verifying that past outputs do not change.
NoFutureData is local and deterministic. The core has no mandatory runtime dependency or network service; the dataframe join helper uses optional pandas.
Reviewer path: start with the research brief, inspect the evaluation and falsification contract, then run
python benchmarks/run_evaluation.py. The committed evaluation is designed to expose misses and failed controls rather than optimize a single headline score.
Use it four ways without adopting a backtesting framework:
nofuture scanin CI/pre-commit for Python and Jupyter source review;audit_availability/as_offor explicit point-in-time data contracts;nofuture audit-manifestfor repository-level dataset availability contracts;- prefix and future-mutation invariance checks for custom feature pipelines.
A timestamp saying when an event happened is not enough for an honest historical simulation. Revised macro data, delayed feeds, edited messages, restated fundamentals, finalized labels, and post-close files can all exist in today's dataset even though they were unavailable at the historical decision time.
NoFutureData uses three deliberately separate concepts:
event_time: when the described event happened or is scheduled to happen.known_at: when the record was first observed or published to the consumer.eligible_from: the earliest time the record is allowed to affect a decision after validation, normalization, latency, or policy constraints.
It does not assume event_time <= known_at. A future calendar event can be
known today. The causal requirement is that the row is available before the
decision that consumes it.
The main branch currently targets 0.5.0.dev0. The latest tagged stable
release is v0.4.0, so the release-wheel and Action examples below intentionally
remain pinned to v0.4.0 until the next release is cut.
Install the signed-off release wheel directly from GitHub:
python -m pip install https://github.com/ORANGINGS/nofuturedata/releases/download/v0.4.0/nofuturedata-0.4.0-py3-none-any.whlThe release also includes SHA256SUMS.txt. For editable development from a
checkout:
python -m pip install -e .The package requires Python 3.10+. The core has no runtime dependency outside
the standard library. For the pandas join helper, install pandas 2.1+ alongside
the release wheel, or use python -m pip install -e ".[pandas]" from a checkout.
PyPI publishing is prepared through GitHub OIDC Trusted Publishing. It will be enabled after the one-time PyPI project/publisher binding is completed; until then the GitHub release wheel above is the canonical install artifact.
from nofuturedata import audit_availability
rows = [
{
"event_time": "2026-09-20T12:00:00+00:00",
"known_at": "2026-09-16T09:00:00+00:00",
"eligible_from": "2026-09-16T09:01:00+00:00",
"decision_time": "2026-09-16T09:00:30+00:00",
}
]
report = audit_availability(rows, decision_time="decision_time")
assert not report.ok
print(report.to_dict())The row fails because eligible_from is later than the historical decision.
CSV works from the CLI:
nofuture audit-csv features.csv --decision-time decision_timeNaive timestamps without a timezone fail closed.
from nofuturedata import as_of
visible = as_of(records, "2026-09-16T09:30:00+00:00")eligible_from is used first, with known_at as the fallback. If neither is
present, the query raises instead of silently treating today's data as
historically available.
from nofuturedata import point_in_time_join
joined = point_in_time_join(
decisions,
vintages,
decision_time="decision_time",
by=["series", "event_time"],
)The helper always uses backward/as-of semantics: a vintage cannot match a
decision made before that vintage's eligible_from (or known_at fallback).
All timestamps must carry a timezone. Duplicate right rows with the same by
keys and availability timestamp fail closed instead of letting dataframe row
order silently choose a revision.
Commit a small JSON manifest next to the data contract:
{
"schema_version": 1,
"datasets": [
{
"path": "data/vintages.csv",
"format": "csv",
"event_time": {
"column": "event_time",
"semantics": "observation_period"
},
"known_at": {
"column": "known_at",
"semantics": "first_observed_by_consumer"
},
"eligible_from": {
"policy": "explicit_column",
"column": "eligible_from"
},
"timezone": "offset-aware",
"revision": {
"policy": "append_only_vintages",
"key": ["series_id", "event_time"]
},
"null_reason": {
"policy": "required_when_value_missing",
"column": "null_reason",
"value_columns": ["value"]
}
}
]
}Then validate the referenced data:
nofuture audit-manifest temporal-contract.jsonThe validator requires explicit event-time and known-at semantics, an eligible-from policy, offset-aware timestamps, a revision policy, and a null reason policy. It also checks required columns, optional decision-time causality, ambiguous revision timestamps, and missing-value explanations. Dataset paths are relative to the manifest, making the contract portable in CI. See the manifest reference and the portable JSON Schema for the full format.
nofuture scan src/Multiple paths are accepted, so the same command works naturally with pre-commit's filename passing:
nofuture scan src tests examples/safe_pipeline.pyMissing paths and an entirely empty scan fail closed instead of returning a misleading PASS.
Python code cells inside .ipynb files are scanned too. Findings report the
notebook cell number; markdown and non-Python notebooks are ignored.
The repository also ships a tiny runnable notebook corpus under
notebooks/fixtures/: one leaking and one safe notebook
for each shipped semantic source rule. CI checks exact rule IDs, notebook cell
numbers, fixture contents, paired rule coverage, and the context-only behavior of
SRC011/SRC012:
python benchmarks/run_notebook_corpus.pyCurrent notebook-fixture result: 24/24 cases pass across 12/12 paired semantic rules. These are deterministic conformance examples, not a real-world recall estimate.
Current static rules flag:
- negative
shift(...), such asshift(-1); bfill()/backfill();- centered rolling windows;
merge_asof(..., direction="forward"|"nearest");- negative
diff(...)/pct_change(...)periods; fillna(method="bfill"|"backfill");- backward or bidirectional interpolation;
- same-dataframe whole-series feature aggregates;
- negative absolute
.iloc[-N]indexing; - fixed/day resample aggregations labeled at the left interval edge.
Some risks are only meaningful once the data semantics are known. Opt into time-series evaluation context when scanning forecasting/backtest evaluation code:
nofuture scan src --time-seriesThis enables SRC011 for generic/random cross-validation patterns such as
KFold, ShuffleSplit, shuffled train_test_split, and scikit-learn
evaluation/search helpers whose cv is omitted, None, or a literal integer.
The same syntax remains clean in the default scanner because it can be valid for
IID data. Explicit TimeSeriesSplit, train_test_split(..., shuffle=False), and
unresolved dynamic cv=cv expressions remain clean in this mode.
These are review gates, not proofs of a bug. Some pipelines use these operations legitimately when building labels. The point is to force the temporal assumption to be explicit.
When a future-looking operation is intentional, suppress the exact rule on that line so reviewers can see the exception in source control:
label = price.shift(-1) > price # nofuture: ignore[SRC001]# nofuture: ignore suppresses all NoFutureData findings on that line. Prefer
the rule-specific form when possible.
Example:
FAIL: 1 finding(s), 5 row/line(s) scanned
[SRC001] negative shift can read future rows (line 5)
For GitHub Code Scanning or another SARIF consumer:
nofuture scan src tests examples/safe_pipeline.py --sarif nofuturedata.sarifStatic scanning cannot catch arbitrary code. Runtime invariance checks can.
from nofuturedata import prefix_invariance, future_mutation_invariance
def cumulative(rows):
total = 0
out = []
for row in rows:
total += row["x"]
out.append(total)
return out
rows = [{"x": n} for n in range(1, 100)]
assert prefix_invariance(cumulative, rows).ok
assert future_mutation_invariance(cumulative, rows).okprefix_invariance recomputes on historical prefixes and compares them with the
same prefix from the full run. future_mutation_invariance changes only future
numeric values and verifies that already-produced output remains identical.
This catches classes of leakage that simple source scanning misses.
For constrained inputs, pass both a domain-preserving mutator and an
input_validator. The mutation check rejects an intervention if it changes the
historical prefix, changes row count, or violates the declared input contract,
instead of treating an invalid counterfactual as evidence of leakage.
report = future_mutation_invariance(
transform,
rows,
mutator=bounded_future_mutator,
input_validator=valid_probability_rows,
)The committed mutation-validity benchmark applies the same contract to bounded probabilities, OHLC candles, simplex-normalized vectors, and monotonic cumulative counters. Each domain has a negative control that must reject an invalid counterfactual before transform behavior can be interpreted as leakage evidence. Current mutation-validity result: 13/13 checks passing.
The repository includes a deterministic corpus of intentionally leaking and safe
controls. It exercises every shipped semantic static rule (SRC001+) and is
executed on every CI run:
python benchmarks/run_benchmark.pyThe current corpus contains 31 independently checked cases: 18 planted leaks and
13 safe controls. It now includes opt-in time-series cases for SRC011 and
SRC012 alongside their chronological/causal controls. A case passes only when
the emitted rule IDs exactly equal its
declared expectation. The benchmark also reports finding-level precision,
recall/F1, leak-case detection rate, safe-case specificity, and per-rule coverage.
These are conformance metrics on a planted corpus, not estimates of recall on
arbitrary real-world temporal leakage.
Run the full research evaluation (static conformance, method ablation, paired notebook conformance, downstream metric inflation, intervention validity and sensitivity, real-pandas behavioral transfer, revision/vintage robustness, and external reproductions) with one command:
python -m pip install -e ".[pandas,dev]"
python benchmarks/run_evaluation.pyThe research suite installs the optional pandas extra because the generative
revision/vintage experiments exercise the real point_in_time_join helper. The
developer extra also pins Hypothesis for property-based counterexample search;
the core package itself remains dependency-free.
Use --json for a single machine-readable report suitable for CI artifacts or
independent review. A suite PASS means the reviewed benchmark contracts still
reproduce; it does not mean the external scanner has perfect recall.
CI also runs python benchmarks/check_documented_metrics.py. That check derives
headline numbers from the live evaluator and fails when reviewer-facing README,
research, evaluation, or landing-page metrics drift from those results.
The Python 3.12 CI job also uploads the full run_evaluation.py --json output as
a machine-readable workflow artifact for independent review.
The source distribution carries the benchmark suite, reviewer-facing research docs, schemas, examples, and fixtures. CI extracts the built sdist and reruns this same evaluation and documentation-metric gate from the packaged source tree before the wheel smoke test.
The project asks whether a small repository-local guard can make temporal causality assumptions testable without requiring a feature store, backtesting engine, or ML framework. The evaluation is intentionally split by evidence strength:
- Static conformance: every shipped rule needs a planted leak and a safe counter-example, and the emitted rule IDs must match exactly.
- Behavioral causality: prefix and future-mutation checks test whether past outputs actually change when only future inputs are removed or perturbed.
- Data availability: manifest and point-in-time checks test whether each record was known and eligible before the decision that consumed it.
The project treats counter-examples as first-class evidence. A real-world leak that passes the current checks becomes a false-negative reproduction to preserve before extending a rule. A legitimate pipeline that is flagged becomes a false-positive reproduction and should narrow or remove the rule. Adoption data and externally contributed reproductions are required before making claims about real-world recall.
The repository also includes a deterministic method-comparison experiment:
python benchmarks/method_comparison.pyIts six cases include explicit pandas lookahead patterns, indirect future dependencies that are intentionally outside the AST rule set, and safe controls. On this ablation corpus, the static rules detect 2/4 leak cases with no safe-case false positives, while the union of prefix and future-mutation invariance detects 4/4 leak cases and both safe controls remain clean. The combined method therefore also detects 4/4. This is evidence of complementarity, not an estimate of real-world recall: static analysis can review code without executing it, while behavioral checks require a runnable transform and representative input rows.
The repository includes a fixed-seed synthetic forecasting experiment that asks what the leak can do to an otherwise ordinary chronological holdout:
python benchmarks/downstream_metric_inflation.pyThe same OLS model and 1,600/800 train/test split are evaluated twice. The causal
model uses only lag/current observations; the leaking model adds the next
observation as an unavailable feature. Synthetic downstream impact: causal
chronological holdout R² 0.630 vs leaked 0.991 (Δ +0.361), with the
leaked RMSE falling to 0.158× the causal RMSE. The safe lag source stays clean
and the future shift emits SRC001.
This experiment demonstrates a mechanism by which temporal leakage can make an offline result look dramatically stronger. It is a fixed synthetic example, not an estimate of real-world metric inflation or expected model performance.
The manifest contract also has a fixed-seed generative benchmark:
python benchmarks/revision_vintage_robustness.pyIt generates 24 different append-only vintage histories with explicit positive eligibility delays. Each trial checks manifest acceptance in original and shuffled row order, duplicate-availability rejection, pre-eligibility selection, and post-eligibility latest-vintage selection after the right-side rows are shuffled. The current deterministic result is 120/120 checks passing. This is reproducible generative coverage of the revision contract, not a claim that every possible vintage history has been exhausted.
The same five core invariants plus seven multi-column/null edge invariants are also searched with Hypothesis rather than a fixed sample:
python benchmarks/property_revision_vintage.pyHypothesis varies series count, observation count, vintage count, eligibility delay, row permutation, duplicate insertion position, 2–4 segment multi-column revision keys, null-value reasons, and missing-key injections, and shrinks a failing case to a smaller counterexample. Hypothesis property result: 768/768 invariant evaluations pass across 64 generated examples. This broadens the search space without turning a finite property test into a proof over arbitrary histories.
The domain-valid mutation conclusions are also stressed across a fixed grid:
python benchmarks/intervention_sensitivity.pyThe current result is 48/48 checks passing across four input domains, three historical cut points, and two valid mutation strengths. Every grid point must keep the causal transform clean and detect the planted future dependency; the benchmark does not tune or select a favorable intervention after seeing results.
The behavioral checks are also exercised against executable pandas operations, instead of only hand-written transforms that mimic the same causal shape:
python benchmarks/behavioral_generalization.pyReal-pandas behavioral transfer: 18/18 cases match; runtime checks detect 9/9 leaks and keep 9/9 safe controls clean.
The original trailing rolling, expanding, and exponentially weighted controls
remain clean while centered rolling, backward fill, and full-series centering
remain detected. The expanded set adds interleaved panel rows and irregular
timestamps: grouped expanding/EWM and irregular forward fill stay causal, while
group-wide mean, grouped backward fill, and time interpolation are detected.
It now also pairs backward/forward merge_asof, right-closed/right-labeled vs
left-labeled 15-minute resampling, and two-column historical vs future diff
pipelines. The grouped/irregular mutation contract changes only future x
values while preserving timestamp order and group identity; the new multi-column
contract mutates both future numeric features while preserving row identity and
time order. This is a transfer check over 18 executable pandas operations, not a
claim of perfect behavioral recall on arbitrary pipelines.
benchmarks/external_reproductions.json keeps a second corpus derived from
public documentation rather than examples invented for this project. It includes
Freqtrade's documented negative-shift, whole-dataframe aggregation, absolute
iloc, resampling, and safe trailing-window patterns; pandas forward/backward
fill semantics; scikit-learn's time-series cross-validation guidance; Polars,
Xarray, Dask, PySpark pandas, and Snowpark pandas shift/diff semantics; and NumPy
circular roll semantics. Each entry records its source URL and the detector
behavior reviewed for the current version.
python benchmarks/run_external_reproductions.pyOn this small documentation-backed corpus the source-only scanner detects 12 of 21
documented leak cases and keeps all 17 safe controls clean. With declared
time-series context it detects 21 of 21. SRC010 covers the narrow fixed/day
resampling case when an aggregate is labeled at the left interval edge, and the
existing negative-shift/diff rules also transfer across independently maintained
Polars, Xarray, Dask, PySpark pandas, and Snowpark pandas APIs. Two source-only
scikit-learn single-level CV misses and the nested GridSearchCV +
cross_val_score reproduction become conditional SRC011 detections under
explicit time-series context. GroupKFold and GroupShuffleSplit are also
context-gated for panel forecasting because grouping alone does not impose
chronological train-before-test order. The same opt-in rule now covers
learning_curve, validation_curve, and permutation_test_score when they use
default/integer CV; paired explicit TimeSeriesSplit controls remain clean. The
nested control uses explicit TimeSeriesSplit at both levels and remains clean.
The existing negative-shift rule also transfers to Xarray when a negative offset
is attached to an explicitly temporal dimension such as shift(time=-1); the
paired shift(time=1) and non-temporal axis=-1 controls remain clean.
NumPy np.roll(values, -1) is now covered by the opt-in SRC012 rule only when
time-series context is declared. The default source-only scan still leaves it
clean because circular roll is not inherently temporal, while the paired
positive-roll lag control remains clean under temporal context. This change was
made against the previously published miss and paired control rather than by
broadening every .roll(...) call. These curated, non-random rates are not
estimates of real-world recall or specificity.
benchmarks/reported_cases.json is kept separate from the documentation-backed
API corpus. It turns public GitHub issue reports into a small regression set while
preserving the strength of each source instead of treating every report as a
maintainer-confirmed diagnosis:
python benchmarks/run_reported_cases.pyGitHub-reported regression result: 3/3 reviewed baselines match; layered checks surface 2/2 reported leaks, one source-only miss is recovered by the availability contract, and one maintainer-confirmed safe callback remains a documented static false-positive boundary.
- Freqtrade issue #12507
reports an unclosed higher-timeframe candle entering a
15-minute decision through
merge_asof(direction="backward"). The static syntax is clean because backward joins are normally causal; once the reported higher-timeframe value is modeled with its real availability time,audit_availability()emitsLEAK001. - CryptoMarket_Regime_Classifier issue #1
reports current-bar features paired
with a next-bar target before the current bar is complete. The negative target
shift emits
SRC001, and the stated bar-completion timing emitsLEAK001. - Freqtrade issue #12168
documents the opposite boundary: a maintainer explains
that callback data are truncated to the current backtest time, so
dataframe.iloc[-1]can be safe there.SRC009still flags the syntax because NoFutureData does not infer framework callback semantics; the case is retained as a known false positive rather than hidden from the evaluation.
The two leak cases above originate in reporter-authored issues. Their inclusion proves only the reviewed response to the stated timing contracts, not that the upstream maintainers accepted every diagnosis or that these three cases estimate real-world error rates.
Downstream projects can put a source scan in CI:
name: temporal-leakage
on: [push, pull_request]
jobs:
nofuture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ORANGINGS/nofuturedata@v0.4.0
with:
path: srcThe unreleased 0.5.0.dev0 source adds an optional time-series: "true"
Action input matching the CLI --time-series context. Keep public workflows on
the tagged v0.4.0 interface until a 0.5 release is cut; reviewers testing this
checkout can exercise the new input with the repository-local uses: ./ Action.
To publish findings in GitHub Code Scanning, grant security-events: write and
turn on SARIF upload:
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
- uses: ORANGINGS/nofuturedata@v0.4.0
with:
path: .
sarif: nofuturedata.sarif
upload-sarif: "true"repos:
- repo: https://github.com/ORANGINGS/nofuturedata
rev: v0.4.0
hooks:
- id: nofuturedataThen run pre-commit run --all-files. The hook receives changed .py and
.ipynb files directly and fails closed when it finds a temporal leakage gate.
It is not a backtesting engine and does not decide whether a model is good. It checks a narrower prerequisite: whether the historical information boundary is internally consistent.
It also does not claim that passing these checks proves absence of all leakage. Data revisions, survivorship bias, target construction, cross-validation, and provider-specific publication semantics still need domain-specific review.
- Availability is data. Preserve when information became usable, not only the event timestamp.
- Fail closed. Missing causal timestamps are an error when the pipeline claims point-in-time safety.
- Mutation beats naming. A column named
holdoutproves little; changing future data and observing whether history changes is stronger evidence. - Small auditable rules. Static checks explain exactly what pattern caused the finding.
- Provider-agnostic core. Finance is one use case; the same problem appears in forecasting, recommender systems, operations, experimentation, and ML.
- Why this matters — evidence for temporal leakage as a cross-domain reproducibility and engineering problem, plus the scope of this tool.
- Research brief — research question, contribution, current evidence, negative result, and next falsifiable experiments.
- Evaluation and falsification — reproducible benchmark results, method ablation, external reproductions, context-dependent rules, and claim limits.
- Rule reference — stable rule IDs, rationale, suppressions, and runtime invariance semantics.
- Temporal contract manifests — repository-level dataset availability and revision contracts.
- PyPI publishing — tokenless OIDC release workflow and the one-time Trusted Publisher setup.
- Roadmap — next candidate capabilities and adoption evidence.
See CONTRIBUTING.md. Synthetic minimal reproductions are preferred. New static rules need both a leaking example and a safe counter-example.
MIT.