Skip to main content

wouldcatch: your tests pass. Would they catch a bug?

CI PyPI Python 3.10+ Zero dependencies Windows, macOS, Linux MIT license

Quick start · The problem · What it mutates · CI and agents · FAQ


wouldcatch plants small bugs in only the lines you just changed, runs your tests after each one, and tells you which bugs got through. If your tests stay green when the code is wrong, you'll know the exact line.

wouldcatch finds 3 bugs the tests miss, then 0 after one more test

Quick start

pipx install wouldcatch        # Windows without pipx: py -m pip install wouldcatch
cd your-repo
wouldcatch

That's it. No config file, no plugin, zero dependencies. It reads your git diff, runs pytest, and cleans up after itself.

JavaScript and TypeScript work the same way, with one extra piece (a parser):

pipx install "wouldcatch[js]"
cd your-repo
wouldcatch                     # runs your package.json "test" script per mutant

It handles .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts and .cts, and picks npm, pnpm, yarn or bun from your lockfile. Jest, Vitest, Mocha and node --test all work, since it only needs a command that exits non-zero when a test fails.

The problem

Two tests. Every line runs. Everything is green.

def discount(price, is_member):              def test_member():
    if is_member and price >= 100:               assert discount(200, True) == 190
        return price - 10
    return price                             def test_guest():
                                                 assert discount(50, False) == 50
$ wouldcatch
  MISSED  shop.py:2  `and` -> `or`
  MISSED  shop.py:2  `>=` -> `>`
  MISSED  shop.py:2  `100` -> `101`

4/7 caught. 3 bug(s) your tests would not notice.
Mutant The bug it stands for Why the tests missed it
>= -> > A member spending exactly 100 gets no discount No test checks 100
100 -> 101 Same boundary, shifted by one No test checks 100
and -> or Every guest over 100 gets the discount No test has a guest over 100

Add two asserts, discount(100, True) == 90 and discount(200, False) == 200, and it's 7/7 caught.

Coverage said 100%. Coverage counts which lines ran. It can't tell you whether any test would fail if those lines were wrong. That's the question wouldcatch answers.

Why only the diff

Mutation testing is the honest way to grade tests, but running it across a whole project takes long enough that almost nobody does. wouldcatch only touches the lines in your diff. A normal change gives 5 to 30 mutants, so it's done before you finish the commit message.

Line coverage Whole-project mutation testing wouldcatch
Tells you a test would fail on a bug no yes yes
Time on a typical change seconds minutes to hours seconds to minutes
Setup plugin config files none
Output a percentage a report on everything the exact lines to test

What it mutates

One change at a time, only on lines in your diff. Test files are never mutated.

Kind Example Catches tests that never check...
Comparison a < b -> a <= b, == -> !=, in -> not in boundaries and equality
Arithmetic + -> -, * -> /, += -> -= the actual computed value
Logic and -> or, not x -> x each side of a condition
Constants True -> False, 100 -> 101 exact values and limits
Returns return total -> return None the return value at all
Calls max(a, b) -> min(a, b) which of two values wins
Loops break -> continue how a loop ends early
Branches if cond: -> if not (cond): both paths of an if or while

JavaScript and TypeScript get the same set in their own spelling: === -> !==, && -> ||, !x -> x, true -> false, return x -> return undefined, Math.max -> Math.min, if (cond) -> if (!(cond)). Type annotations, generics and JSX tags are never touched.

Want another one? New mutations are small PRs: see the good first issues.

Usage

Command What it does
wouldcatch Uncommitted changes; on a clean feature branch, everything since it left main; otherwise the last commit
wouldcatch --base main Everything on this branch since main
wouldcatch --cmd "pytest tests/unit -x -q" Use your own test command
wouldcatch --json Machine-readable output for scripts and agents
wouldcatch --all Show caught mutants too
wouldcatch --github Also print GitHub annotations (automatic inside GitHub Actions)
wouldcatch --max 100 Try more mutants (default 50)

Exit codes: 0 nothing missed · 1 something missed · 2 setup problem (not a git repo, tests already failing) · 3 incomplete: --max left mutants untried (pass --allow-partial to accept that).

A mutant whose test run times out is reported as timeout, separately from caught: a hang isn't proof that a test checks that line.

Skip a line on purpose with # wouldcatch: ignore.

CI, pre-commit and AI agents

GitHub Action: missed bugs show up on the exact lines of the pull request
on: pull_request
jobs:
  wouldcatch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
      - uses: actions/setup-python@v6
        with:
          python-version: "3.13"
      - run: pip install -e . pytest   # your project and its test deps
      - uses: Sahil-u07/wouldcatch@v0.3.0

Inputs (all optional): cmd (test command), base (defaults to the PR's base branch), max (default 50), allow-partial (default false), fail-on-missed (default true).

pre-commit: check before every commit

With wouldcatch installed in your project environment:

- repo: https://github.com/Sahil-u07/wouldcatch
  rev: v0.3.0
  hooks:
    - id: wouldcatch
AI coding agents: make them write tests that check something

Agents are good at writing tests that pass and bad at writing tests that would fail on a bug. The MISSED list gives them a concrete target.

Claude Code: copy skills/wouldcatch into .claude/skills/ in your project.

Any other agent: add this to AGENTS.md:

After changing Python, JavaScript or TypeScript code, run `wouldcatch --json`. For every mutant with
"status": "missed", add a test that fails for that change. Re-run until none are missed.
Never change the source code to make a mutant go away.

How it works

git diff ──> changed lines ──> mutants ──> run tests per mutant ──> restore file ──> report
  1. git diff gives the Python, JavaScript and TypeScript lines you changed.
  2. Each mutant is a tiny text edit at an exact position, so your formatting and comments stay untouched.
  3. Your test command runs once per mutant. A fresh bytecode cache each run means a stale .pyc can never hide a mutant.
  4. The file is restored byte for byte (CRLF included) after every run, even on Ctrl+C. If the process is killed, the next run restores it from a backup kept inside .git first.

FAQ

Isn't this what mutmut or pytestmut already do?

Partly, and they're good tools. mutmut and cosmic-ray mutate the whole project, which is a thorough audit. pytestmut also has a diff mode (--diff, --staged) and parallel workers, so if you need speed on a big suite, try it. Diff-based mutation testing isn't new in other languages either: Infection (PHP) and Mull (C/C++) both do it.

wouldcatch aims at one workflow: zero config, run it on the change in front of you, get exact lines back. That includes warnings on the pull request diff through the GitHub Action, and a --json list an AI agent can turn into tests.

Will it mess up my files?

It edits one file at a time and restores it right after, byte for byte. If the process dies mid-run, the next run restores the file from a backup kept inside .git before doing anything else.

My test suite is slow.

Each mutant is one run of your test command. Point --cmd at the relevant tests and add -x so pytest stops at the first failure.

A mutant can't be caught because the code really doesn't care.

That happens (a log interval, a cache size). Mark the line # wouldcatch: ignore.

Which languages?

Python out of the box. JavaScript and TypeScript with pip install "wouldcatch[js]", which adds the tree-sitter parser. A repo with both gets both checked in one run.

Two limits on the JS/TS side for now: the default command needs a package.json at the repo root (in a monorepo, pass --cmd), and if your test runner type-checks (ts-jest does), a mutant that only breaks the types counts as caught.

Contributing

PRs welcome, especially new mutations. Start with a good first issue and read CONTRIBUTING.md.


If wouldcatch found a bug your tests missed, a star helps other people find it.

MIT · made by Sahil Lenka

Metadata

Release files for wouldcatch 0.3.0

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

Source distribution (sdist)

Source distribution for wouldcatch 0.3.0
File Size Uploaded Provenance Actions
wouldcatch-0.3.0.tar.gz 27.2 kB With provenance Details for wouldcatch-0.3.0.tar.gz

Built distribution (wheel)

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

Total release size: 45.8 kB

Release files / wouldcatch-0.3.0.tar.gz

Download URL wouldcatch-0.3.0.tar.gz
Size 27.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5325bef5760db0ff191d0a79951e05cd7af1bb2f2ef42abe0e8849227ecec2e6
BLAKE2b-256 checksum
How to use checksums
999db282b7423f873e674c3137c924cb926358f7003377649919303eeb6caf27
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 12, 2026.

Transparency log

Release files / wouldcatch-0.3.0-py3-none-any.whl

Download URL wouldcatch-0.3.0-py3-none-any.whl
Size 18.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
da9f6096fc9ccfd40325158fe6253576ed1701cae540a7f115fc99e883a6ad7f
BLAKE2b-256 checksum
How to use checksums
45ec308d5164d36da8dad831324059b26091c3fef9b1bd7fb0add432188e43e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 12, 2026.

Transparency log

Security for wouldcatch 0.3.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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for wouldcatch-0.3.0.tar.gz

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 Nov 12, 2026.

Transparency log

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.

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