Skip to main content

nodrift

PyPI Python CI License: MIT

Prove a refactor changed nothing — by running it, not by reading it.

You changed 200 lines. Your tests pass. Did anything actually change?

Tests check what someone remembered to check. nodrift compares everything observable: return values, exceptions raised, whether arguments were mutated, and the files it writes — using the real inputs your test suite already produces.

No model reviews the code. The verdict comes from execution.

$ nodrift record --package mypkg
[nodrift] recording 505 callables in mypkg
[nodrift] 18402 distinct inputs across 408 functions -> .nodrift/recording.pkl

$ nodrift check HEAD~1

  3 of 18402 recorded inputs behave differently (1 function)

    mypkg.dates:parse
        3 of 47 inputs differ
        before: [["raise", ["exception", "builtins.ValueError", "invalid date", ...
        after:  [["return", ["object", "datetime.date", ...

That last block is the point. Not "this looks risky"here is the input, here is the difference.

Install

pip install nodrift

Requires Python 3.9+. Tested in CI on Linux, macOS and Windows across Python 3.9, 3.11 and 3.13.

Use

1. Record — run your test suite once. nodrift watches, and writes down the real arguments every function receives.

nodrift record --package mypkg

2. Check — replay those inputs against an older commit and against your working tree, then compare.

nodrift check HEAD~1

Exit code is 0 if nothing changed, 1 if something did — so it drops straight into CI.

To compare two commits without checking either one out, pass both:

nodrift check HEAD~1 HEAD

If your package lives under src/, pass --subdir src.

--verbose names everything the verdict does not cover: functions whose inputs were too large to record, and functions quarantined as nondeterministic.

Recording part of a package

One noisy or vendored module can dominate a recording. Both options take fnmatch patterns against the full module:Qualname target and repeat. Note the :"mypkg.core.*" matches mypkg.core.dates:parse but not mypkg.core:parse, so match the module name without a trailing dot:

nodrift record -p mypkg --exclude "mypkg.vendored*"
nodrift record -p mypkg --include "mypkg.core*"

How many callables the patterns skip is printed, not hidden.

How it works

  1. Record. A pytest plugin wraps every function in your package and pickles the arguments it receives. Deduplicated, capped per function.
  2. Replay. Both versions are exported to the same directory path, one after the other, and every recorded input is replayed against each.
  3. Compare. Outcomes are reduced to a structural fingerprint — stable across processes, order-independent for sets and dicts, with memory addresses scrubbed — and compared.

Two details that matter more than they sound:

  • The baseline is replayed twice. Anything that disagrees with itself is nondeterministic — a clock, a random seed, an id() in a repr — and is quarantined rather than reported. Silence beats a false alarm.
  • Both versions run from the same path. Code that embeds its own file path in output would otherwise look changed when it isn't. Holding seeds constant is not enough; the environment is part of the input.

What it does not do

Being direct, because a tool like this is only worth having if you trust it.

It is not better than your tests at finding bugs. Measured across 118 injected mutations in two libraries, nodrift found exactly one defect their test suites missed. Use it to check that a change is inert, not to hunt for bugs.

It only sees what your tests already run. No coverage there, no signal here.

Some functions cannot be recorded at all. Code that passes large object graphs around — parsers, AST walkers, tree transformers — produces arguments too big to capture. On sqlparse, 40 core functions were skipped for this reason. nodrift reports them as not covered rather than pretending otherwise, but the gap is real.

Side effects are only partly captured. File writes are compared (path, mode, size and a hash of the bytes). Network calls and database queries are still invisible.

Recording is slow. Expect roughly a 4x slowdown on the recorded run. Tests that assert on wall-clock time may fail while recording.

Measured behaviour

Two libraries, chosen as opposites — one with an exceptional test suite, one with an ordinary one.

packaging sqlparse
Test suite 62,424 tests 494 tests
Recorded inputs 184,592 15,830
Functions covered 408 141
False positives on identical code 0 0
Injected mutations caught by nodrift 50 / 61 52 / 57
Injected mutations caught by the test suite 57 / 61 52 / 57

Zero false alarms is the property the tool lives or dies by, and it holds on both.

On sqlparse the two are exactly tied, and nodrift missed nothing the tests caught — it matched a real suite's sensitivity without anyone writing the assertions. On packaging, whose suite is far stronger than most, it trailed; it did find one bug all 62,424 tests miss (an error reporting has invalid data for a field that was merely unrecognised), but one find in 118 mutations is not a bug-hunting tool. It is a safety net.

Contributing

Contributions are very welcome, and the work is deliberately easy to pick up — most of it touches one file and needs one test.

Start with the good first issues. The broad areas that need help:

  • Side-effect capture — intercept requests, sqlalchemy
  • Comparators for types that still need a policy — datetimes and UUIDs are settled: quarantined, not normalised (issue #12)
  • Performance — recording currently costs ~4x
  • Framework adapters beyond pytest

The most valuable bug report of all is a false positive: if nodrift check reports a change on code you did not change, please open an issue.

See CONTRIBUTING.md for setup and how the pieces fit together.

git clone https://github.com/LuShadowX/nodrift
cd nodrift
uv venv && uv pip install -e ".[dev]"
pytest

Licence

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nodrift-0.1.4.tar.gz (39.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nodrift-0.1.4-py3-none-any.whl (29.3 kB view details)

Uploaded Python 3

File details

Details for the file nodrift-0.1.4.tar.gz.

File metadata

  • Download URL: nodrift-0.1.4.tar.gz
  • Upload date:
  • Size: 39.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for nodrift-0.1.4.tar.gz
Algorithm Hash digest
SHA256 8245dd582640e4cef296a2dba7a55b55ad3cf0f5f8bb38ba57d1a2f9b316f775
MD5 09f2482866f3d6f40444bf098453a8d4
BLAKE2b-256 3e9b08bf28e510eb79c5147321917e93c830a8425bcc9d0cbf098cb22eca09d0

See more details on using hashes here.

File details

Details for the file nodrift-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: nodrift-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 29.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for nodrift-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 8e4d5ea46b9810a3be58312e1ec3a6e8e34e9c5bc2d589d83c66c5621d900eda
MD5 d32065baddd8f26caadf737acac3f564
BLAKE2b-256 daca9357baed6bd3c96560376233f3a2bf890b9ac67b7f0c92b8af40dc6dbaa5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page