Skip to main content

pdrift

PyPI CI License: MIT

You changed a prompt (or a model version) and now you don't know which of your LLM outputs silently changed meaning. pdrift is snapshot testing for prompts — like jest snapshots, but judged semantically instead of byte-by-byte.

pip install pdrift
# pdrift_answers.py
from pdrift import case

@case(inputs="cases.jsonl")          # one JSON object per line: {"id": "...", "input": "..."}
def answers(input: str) -> str:
    return my_llm_call(input)        # any function that returns a string
pdrift snapshot   # run the suite, record baseline outputs
pdrift check      # re-run and flag *meaningful* drift — exit 1 in CI

See it catch a flipped meaning

A one-word prompt tweak turns "found no link" into "confirmed a strong link". Exact-match snapshots scream at every harmless rephrase; humans skim and miss the real one. pdrift buckets each case by embedding similarity — actual output:

$ pdrift check
                         pdrift check (threshold 0.9)
+----------------------------------------------------------------------------+
| suite | identical | trivial | meaningful | new case | missing case | error |
|-------+-----------+---------+------------+----------+--------------+-------|
| llm   |         0 |       3 |          2 |        0 |            0 |     0 |
+----------------------------------------------------------------------------+
                           meaningful changes - llm
+-----------------------------------------------------------------------------+
| case          |    sim | baseline vs current                                |
|---------------+--------+----------------------------------------------------|
| coffee-study  | 0.7449 | - The study found no link between coffee           |
|               |        | consumption and heart disease.                     |
|               |        | + The study confirmed a strong link between coffee |
|               |        | consumption and heart disease.                     |
| deploy-status | 0.5679 | - The deployment completed successfully and no     |
|               |        | downtime was reported.                             |
|               |        | + The deployment failed and caused significant     |
|               |        | downtime across all regions.                       |
+-----------------------------------------------------------------------------+
Drift detected.
# exit code 1

The three rephrasings ("Hello! How can I help you today?" → "Hi there! How can I assist you today?", sim 0.9684) landed in trivial — exit code stays 0 for those. The two flipped meanings got flagged. That's the whole tool.

No server, no dashboard, no API key, no YAML pipeline — a dev tool, not a platform. Baselines are JSON files in your repo; the check is a CLI command with an exit code.

Why this doesn't drown you in false positives

Two things make pdrift trustworthy where naive semantic diffing isn't:

1. Local embeddings — free, offline, no API key. Similarity is computed with fastembed (ONNX, BAAI/bge-small-en-v1.5) on your machine. Checking costs zero dollars and zero network calls, so you can run it on every commit. Embeddings are cached per suite (keyed by output hash) — repeated checks don't even re-embed.

2. The noise floor — the tool learns each case's natural variance. LLMs at temperature > 0 rephrase themselves constantly. Take multiple baseline samples and pdrift measures how much the baselines differ from each other — the noise floor. A new output is flagged only if it's more different from the baselines than they are from each other:

pdrift snapshot --samples 3
                          meaningful changes - noisy
+-----------------------------------------------------------------------------+
| case       |    sim | noise floor | diff                                    |
|------------+--------+-------------+-----------------------------------------|
| water-boil | 0.6407 |      0.9633 | - The boiling point of water at sea     |
|            |        |             | level is 100 degrees Celsius.           |
|            |        |             | + Water never boils no matter how hot   |
|            |        |             | it gets; boiling is impossible.         |
+-----------------------------------------------------------------------------+

This case's baseline samples agree with each other at 0.9633; the new output only manages 0.6407 against the closest one — flagged, with the numbers shown so you can see why. Meanwhile an honest paraphrase scoring 0.95 sails through, because that's within the case's own noise. No hand-tuned per-case thresholds.

How it works

Each case lands in exactly one bucket:

verdict meaning exit code
identical exact string match to a baseline sample (embeddings skipped entirely) 0
trivial differs, but similarity ≥ min(noise floor, threshold) 0
meaningful more different from the baselines than they are from each other 1
new case in the JSONL but not in the baseline 0
missing case in the baseline but gone from the JSONL 1
error the target function raised (recorded, never crashes the run) 1

Baselines are pretty-printed, key-sorted JSON in .pdrift/<suite>/baseline.json — commit them, and prompt-output changes show up as reviewable diffs in PRs. pdrift accept promotes the latest check run to the new baseline after you've reviewed a change.

JSON outputs get a structural diff

If both baseline and current outputs parse as JSON, pdrift skips embeddings and diffs the structure — keys added/removed, values changed, with dotted paths:

                       meaningful changes - jsonapi
+-------------------------------------------------------------------------+
| case    | sim | noise floor | diff                                      |
|---------+-----+-------------+-------------------------------------------|
| profile |   - |           - | ~ user.address.city: "Berlin" -> "Munich" |
|         |     |             | - removed user.email                      |
+-------------------------------------------------------------------------+

String values longer than 40 chars (summaries, bios) fall back to embedding similarity, so a rephrased description doesn't fail your schema check.

Configuration (optional — zero config needed)

CLI flags override pdrift.toml, which overrides defaults. The report header shows the effective value and where it came from.

# pdrift.toml — everything optional
# threshold = 0.90                    # similarity at/above which a change is trivial
# samples = 1                         # baseline runs per case (3+ enables the noise floor)
# model = "BAAI/bge-small-en-v1.5"    # any fastembed-supported model

# [suite.summarizer]                  # per-suite overrides
# threshold = 0.85
# samples = 5

pytest plugin

Installed automatically. Each case becomes a pytest test:

pytest --pdrift                    # MEANINGFUL = fail, TRIVIAL/IDENTICAL = pass
pytest --pdrift --pdrift-path prompts/

Missing baseline → the case is skipped with "run pdrift snapshot first". Failure messages include the similarity, noise floor, and diff. Without --pdrift the plugin does nothing.

CI: fail PRs on meaningful drift

# .github/workflows/prompt-drift.yml
name: prompt-drift
on: pull_request
jobs:
  pdrift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pdrift
      - run: pdrift check prompts/   # exits 1 on meaningful changes

Because baselines live in git, the reviewable prompt-output diff is right there in the PR alongside the code change that caused it.

FAQ

Why local embeddings instead of an LLM judge or an embeddings API? Cost and trust. A check that costs money per run doesn't get run. Local ONNX embeddings are free, deterministic, offline, and fast enough to run on every commit. The first check downloads the model (~130 MB) once; after that, no network at all.

My outputs are non-deterministic. Won't every check fail? That's the noise floor's job. Snapshot with --samples 3 (or more): pdrift measures how much your own baselines disagree and only flags outputs that fall below that self-similarity. Temperature noise passes; meaning flips don't.

What does a check cost? Zero. No API keys anywhere in the tool. Identical outputs skip embedding entirely, and everything embedded once is cached in .pdrift/<suite>/embeddings.npy.

Why does my JSON suite flag changes at temperature > 0? Honest answer: the JSON structural diff has no noise floor yet — text cases learn their variance from multi-sample baselines, JSON cases don't (#4, planned for 0.2.0). Workaround today: run JSON-producing prompts at temperature 0. --samples does not help JSON suites yet.

Embeddings are weak at negation — can a meaning flip sneak through? Sometimes similarity models score negations higher than humans would. In practice flips we tested score 0.57–0.74 against the 0.90 default — comfortably flagged — but embedding-based comparison is a tradeoff, not magic. Multi-sample baselines tighten the bar further; tune threshold per suite for sensitive cases.

Windows? First-class. Developed on Windows; CI runs the matrix on ubuntu + windows, py3.10–3.12.

License

MIT

Release files for pdrift 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pdrift 0.1.1
File Size Uploaded
pdrift-0.1.1.tar.gz 30.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pdrift 0.1.1
File Interpreter ABI Platform
pdrift-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 53.6 kB

Release files / pdrift-0.1.1.tar.gz

Download URL pdrift-0.1.1.tar.gz
Size 30.3 kB
Tags Source
SHA-256 checksum
How to use checksums
16ef49fcd2ce0b1ddebc2b3ad7bccf97e408dd0821dcf8b4e8ae2ed31bba6ce7
BLAKE2b-256 checksum
How to use checksums
cb95055ed3a51b1464f087acea8d801cfb1aae41f8f425a59f627e0b796d727d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 5, 2026.

Transparency log

Release files / pdrift-0.1.1-py3-none-any.whl

Download URL pdrift-0.1.1-py3-none-any.whl
Size 23.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
661c8a35ff94023c132519a4f96e63c4930c79e2b9be6e40721a02d3f23a9315
BLAKE2b-256 checksum
How to use checksums
42085f1a1b7fcf07f9ff44212e9da9fbfccb38ce29e5838b2dbbf7be0d1b95d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page