Will this dependency upgrade break code you actually wrote? Diffs two versions of a package's API and reports only the changes that hit your call sites.
Project description
willitbreak
Will this dependency upgrade break code you actually wrote?
Dependabot opens eleven pull requests. The changelog for one of them lists forty breaking changes. Thirty-nine are about parts of the library you have never imported, and the fortieth is on line 218 of a file you forgot existed.
There is no good way to find that out today. So people merge and hope, or read the whole changelog, or upgrade in a branch and wait for the test suite to maybe catch it.
pip install willitbreak
willitbreak urllib3 --to 2.2.1
284 API changes in that release. Two of them touch this code. Those two come with file and line numbers; the rest are a number you can ignore.
No dependencies. Python 3.9+.
Why this isn't just a changelog
Every other tool in this space is built for the people publishing a library, so it can tell you what changed but not whether you used any of it. griffe — the most capable of them — puts it plainly in its own documentation: users must review the reported breakages and assess their own codebase for impacts.
That assessment is the work. This does it.
their API diff your source what you get
────────────────── ───────────────── ─────────────────────
284 things changed × 217 call sites = 2 problems, with line numbers
What it takes seriously
Never guessing. A checker that cries wolf gets uninstalled after the second false alarm, and then catches nothing at all. So resolution stops the moment it would have to assume:
client = pkg.Client()
client.get(timeout=5) # resolved: pkg.Client.get
thing = make_it()
thing.get(timeout=5) # not reported — origin unprovable
c = pkg.Client()
c = something_else()
c.get(timeout=5) # not reported — the name was rebound
The same applies to arguments. f(**options) might be passing the parameter
that was removed, or might not, so it is never claimed as a break.
The condition attached to every change. A removed keyword only matters to code that passes it. A parameter that lost its default only matters to code that omits it — pass it positionally and you are fine. A parameter that became keyword-only only breaks callers who passed it positionally. Each of those is checked against the actual call:
urllib3 1.26.18 → 2.2.1
1 file scanned · 284 API changes that do not touch your code
BREAKS urllib3.HTTPResponse
parameter 'strict' was removed
• docs/example/client.py:18 2 positional strict=
BREAKS urllib3.HTTPResponse.from_httplib
method no longer exists
• docs/example/client.py:22 1 positional
2 breaking changes across 2 call sites
Where libraries really live. Almost every package implements in
pkg/_client.py and exposes in pkg/__init__.py. A tool that only looked at
where a class was defined would report pkg._client.Client and never match
the pkg.Client everyone writes, so re-export chains are followed to a fixed
point.
Not importing anything. Both versions are downloaded and read as source. Importing a package to inspect it means executing it, and the version you are asking about is by definition not the one installed — it may not even import on your interpreter.
In CI
- run: pip install willitbreak
- run: willitbreak urllib3 --to ${{ matrix.candidate }} src/
Exit 2 means this upgrade breaks something you wrote. Exit 1 is reserved
for the tool itself failing, so a pipeline can tell those apart and act
differently on each. Exit 0 means the changes do not reach your code.
Isn't this what mypy does?
Partly, and it is worth being honest about the overlap. If a package ships type information, you install the new version, and your code is annotated, mypy will flag a removed attribute.
The differences that matter:
- You have to install the upgrade first. That is the thing you were trying to evaluate, and on a conflict it may not install at all.
- It reports everything, not what changed. A pre-existing error and one introduced by this upgrade look identical.
- It needs types. Untyped packages, and
**kwargs-heavy APIs, are exactly where signature changes hide.
This answers a narrower question — what does this specific version bump do to me — without touching your environment.
Options
| Flag | What it does |
|---|---|
--from VERSION |
Upgrade from this (default: the version installed here) |
--to VERSION |
Upgrade to this (default: latest on PyPI) |
--all |
List every API change, including ones you never touch |
--json |
Machine-readable, for a bot that files the summary |
--import-name NAME |
When the import name differs from the package name |
--cache DIR |
Where downloaded versions live (WILLITBREAK_CACHE) |
--ascii |
No non-ASCII characters |
--color |
auto, always, never. NO_COLOR is honoured |
Paths default to the current directory:
willitbreak httpx --to 0.28.0 src/ tests/
As a library
from willitbreak import check
outcome = check("urllib3", "1.26.18", "2.2.1", ["src"])
for finding in outcome.breaking:
print(finding.change.detail)
for reference in finding.references:
print(" ", reference.source)
Honest limits
- Resolution is one level deep. An instance stored on
self, handed through a factory, or returned from a helper is not tracked, and is silently skipped rather than guessed at. - A package whose API is built at runtime cannot be read from source. Modules that will not parse are reported, never quietly dropped.
- Changed behaviour behind an unchanged signature is invisible to this and to every other static tool. Read the changelog for those.
Development
git clone https://github.com/CAOShurong/willitbreak
cd willitbreak
python -m unittest discover -s tests
The suite runs entirely offline against packages built on disk, so it is fast and does not depend on PyPI being up. Regenerate this README, which is produced by running the tool for real:
python docs/build_docs.py
python docs/build_docs.py --check # what CI runs
CI runs on Ubuntu, Windows and macOS across Python 3.9–3.13, checks this README still matches the output, and verifies the exit codes.
License
MIT. See LICENSE.
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 willitbreak-0.1.0.tar.gz.
File metadata
- Download URL: willitbreak-0.1.0.tar.gz
- Upload date:
- Size: 61.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a7b8cb74630cbfdad0942db02b42134f2970c19c7ccd23e901a25f953bafae4
|
|
| MD5 |
b413fd91507ca7f01dc2268b08e8d91c
|
|
| BLAKE2b-256 |
8b745c9f2fd96c5f61dc8d682a12ebf3a793ce99b419d5befea3999ef90b0c72
|
Provenance
The following attestation bundles were made for willitbreak-0.1.0.tar.gz:
Publisher:
release.yml on CAOShurong/willitbreak
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
willitbreak-0.1.0.tar.gz -
Subject digest:
4a7b8cb74630cbfdad0942db02b42134f2970c19c7ccd23e901a25f953bafae4 - Sigstore transparency entry: 2334677540
- Sigstore integration time:
-
Permalink:
CAOShurong/willitbreak@904c39961952ce16c772e7898d76017796371744 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/CAOShurong
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@904c39961952ce16c772e7898d76017796371744 -
Trigger Event:
push
-
Statement type:
File details
Details for the file willitbreak-0.1.0-py3-none-any.whl.
File metadata
- Download URL: willitbreak-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a895fd92ea897b07c37e040b88e16aa63400b746c1a6785cb9b2a342cebaeaf
|
|
| MD5 |
7416e0a29960401c87e1cd9c72479c0c
|
|
| BLAKE2b-256 |
84fc35f85384b331bdaeabdc213a93b11644315a76814b3aa3f74b9800aba78d
|
Provenance
The following attestation bundles were made for willitbreak-0.1.0-py3-none-any.whl:
Publisher:
release.yml on CAOShurong/willitbreak
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
willitbreak-0.1.0-py3-none-any.whl -
Subject digest:
1a895fd92ea897b07c37e040b88e16aa63400b746c1a6785cb9b2a342cebaeaf - Sigstore transparency entry: 2334677550
- Sigstore integration time:
-
Permalink:
CAOShurong/willitbreak@904c39961952ce16c772e7898d76017796371744 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/CAOShurong
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@904c39961952ce16c772e7898d76017796371744 -
Trigger Event:
push
-
Statement type: