Pytest plugin for regression testing via branch comparison
Project description
pytest-drift
A pytest plugin for regression testing via branch comparison. When a test returns a value, the plugin runs the same test on a base git branch and compares the results — catching regressions before they merge.
How it works
- You run
pytest --drift BASE_BRANCH - For every test that returns a non-None value, the plugin:
- Records the return value from the current branch (HEAD)
- Simultaneously runs the same tests on
BASE_BRANCHin a git worktree - Compares the two results at the end of the session
- Tests returning
None(the default for normal pytest tests) are ignored entirely
The base branch runs in parallel with your HEAD tests, so total wall time is approximately max(HEAD_time, BASE_time) rather than HEAD_time + BASE_time.
Installation
pip install pytest-drift
# With smart DataFrame diff reports (recommended):
pip install "pytest-drift[datacompy]"
Usage
CLI flag
pytest --drift main
pytest --drift origin/main
Environment variable
export PYTEST_DRIFT_BASE_BRANCH=main
pytest
Writing regression tests
Return a value from your test — that's it:
def test_revenue_calculation():
df = compute_revenue(load_data())
return df # compared against the same function on BASE_BRANCH
def test_model_accuracy():
return evaluate_model() # compared as a float
def test_pipeline_output():
return run_pipeline() # compared as a dict, list, DataFrame, etc.
Normal tests (returning None) are unaffected and run as usual.
Comparison logic
The plugin dispatches comparison based on the return type:
| Type | Comparison method |
|---|---|
pd.DataFrame |
Auto-detects join columns; uses datacompy if installed, else pd.testing.assert_frame_equal |
pd.Series |
Converted to DataFrame, same path as above |
float / np.floating |
math.isclose with rtol=1e-5, atol=1e-8 |
np.ndarray |
np.testing.assert_array_almost_equal (5 decimal places) |
dict |
Recursive key-by-key comparison |
list / tuple |
Element-wise comparison |
| Everything else | ==, with repr() diff on failure |
Pandas index auto-detection
When comparing DataFrames, the plugin automatically finds the best join key:
- Named index: if the DataFrame already has a named (non-RangeIndex) index, it's used directly
- MultiIndex: all named index levels are used
- Column heuristic: searches combinations of up to 3 non-float columns with full cardinality (every row is unique in that combination)
- Positional fallback: if no unique key is found, rows are compared positionally
You can also pass join_columns explicitly by calling compare_dataframes directly from pandas_utils.
Terminal output
At the end of the session a regression summary is printed:
========================================================================
REGRESSION COMPARISON SUMMARY
========================================================================
PASSED tests/test_revenue.py::test_revenue_calculation
FAILED tests/test_model.py::test_model_accuracy
Float mismatch:
head: 0.923
base: 0.941
------------------------------------------------------------------------
1 passed, 1 failed (2 total regression comparisons)
How branch switching works
The plugin uses git worktree add to check out BASE_BRANCH into a temporary directory — your working tree is never touched. The worktree is cleaned up automatically after the session.
HEAD tests run ─────────────────────────▶ sessionfinish
│
git worktree add ──▶ BASE tests run in parallel ────┘ compare
Requirements
| Package | Required | Purpose |
|---|---|---|
pytest >= 7.0 |
Yes | Core |
cloudpickle >= 3.0 |
Yes | Serialization of return values |
pandas >= 1.5 |
Yes | DataFrame/Series support |
datacompy >= 0.9 |
Optional | Rich DataFrame diff reports |
pyarrow >= 10.0 |
Optional | Parquet storage for large DataFrames |
Comparison with similar tools
| pytest-drift | syrupy / pytest-snapshot | pytest-regressions | |
|---|---|---|---|
| Baseline source | git branch (live re-run) | committed snapshot file | committed YAML/CSV file |
| Baseline stays fresh | yes — base branch always re-runs | only when you update snapshots | only when you update fixtures |
| Detects environment drift | yes — same code path, different branch | no | no |
| Test changes required | no — just return a value |
yes — use a snapshot fixture | yes — use a regression fixture |
| DataFrame support | yes, with datacompy | via custom serializer | yes, via dataframe_regression |
When to use pytest-drift — you want to catch regressions introduced by your current branch without manually maintaining baseline files. Ideal for data pipelines, model outputs, or any function whose output is hard to specify upfront but easy to compare.
When to use snapshot tools — you want a stable, reviewable artifact in version control. Snapshots are better when the baseline should be human-readable or when you're not working in a git-branch workflow.
Caveats
- The base branch subprocess uses the same Python environment as HEAD — if your project uses
toxornox, point to the correct environment - Session-scoped fixtures with side effects (e.g. starting a server) will run twice — once per session
- Tests that fail on HEAD are not compared (no base result is fetched for them)
- Tests that fail on BASE produce a "base branch test failed, cannot compare" warning
Project details
Release history Release notifications | RSS feed
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 pytest_drift-0.1.2.tar.gz.
File metadata
- Download URL: pytest_drift-0.1.2.tar.gz
- Upload date:
- Size: 18.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47f51496367a61638739dbd8ebe00361962726e4bb6b147be36a0955d16878ce
|
|
| MD5 |
f2305275c35f54a31a6ce377d0c9059a
|
|
| BLAKE2b-256 |
8463a5e313b10942aef21ef862d6614a999d14836ba5183149237cb6e0adfb1b
|
Provenance
The following attestation bundles were made for pytest_drift-0.1.2.tar.gz:
Publisher:
python-publish.yml on jackxxu/pytest-drift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_drift-0.1.2.tar.gz -
Subject digest:
47f51496367a61638739dbd8ebe00361962726e4bb6b147be36a0955d16878ce - Sigstore transparency entry: 1281873382
- Sigstore integration time:
-
Permalink:
jackxxu/pytest-drift@90e28017653653a82037e6070e89092c85eca0f4 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/jackxxu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@90e28017653653a82037e6070e89092c85eca0f4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytest_drift-0.1.2-py3-none-any.whl.
File metadata
- Download URL: pytest_drift-0.1.2-py3-none-any.whl
- Upload date:
- Size: 15.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6690c8360bad954f65e5f13d37f137d32ed8bdef4834efcef4fe94c7deb0643
|
|
| MD5 |
7b7777eebe0e653d422494b9e93f6201
|
|
| BLAKE2b-256 |
1828e7acddc5270fa8584621742024d66914622253bfb09db39b5d87709cbad8
|
Provenance
The following attestation bundles were made for pytest_drift-0.1.2-py3-none-any.whl:
Publisher:
python-publish.yml on jackxxu/pytest-drift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_drift-0.1.2-py3-none-any.whl -
Subject digest:
e6690c8360bad954f65e5f13d37f137d32ed8bdef4834efcef4fe94c7deb0643 - Sigstore transparency entry: 1281873449
- Sigstore integration time:
-
Permalink:
jackxxu/pytest-drift@90e28017653653a82037e6070e89092c85eca0f4 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/jackxxu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@90e28017653653a82037e6070e89092c85eca0f4 -
Trigger Event:
push
-
Statement type: