lenscheck-contract
Your backend keeps a list of what it does. And it can't lie about it.
Companion library to lenscheck-semantic-reviewer. Lenscheck reads code from outside and guesses. This runs inside your app and knows.
Works with Django, Flask, and FastAPI — it reads whichever framework's own runtime state, so routers, blueprints, loops, and dependency trees are all seen exactly as the app resolved them. Models come from Django or SQLAlchemy.
What it does
While your app starts, it records:
- every route (path, method, auth)
- every model (fields, types, constraints)
- every outside call (HTTP, email, payments)
- every background job
- how much of that it actually knows vs. couldn't figure out
Then you can ask your app one question and get an answer:
"What does this app actually do?"
Why it matters
AI opens a PR with 400 changed lines. You don't read them. You read this:
RISKY (4)
POST /orders auth: user -> public
POST /orders new effect: net:analytics.example.com
shop.Customer.email unique: True -> False
shop.Customer.name field removed (data loss)
Four lines instead of four hundred.
Install
pip install lenscheck-contract
Use it — three levels
Level 1: nothing to write
Works on an existing project with zero code changes. Point it at your app:
# Django — reads DJANGO_SETTINGS_MODULE, the URLconf, and the model registry
lenscheck-contract export --settings myproject.settings -o contract.json
# Flask / FastAPI — point at the app object (module:attr, or a create_app factory)
lenscheck-contract export --no-django --app myapp.main:app -o contract.json
lenscheck-contract export --no-django --app "myapp:create_app" -o contract.json
Celery jobs are found automatically when Celery is installed and your tasks are imported; point at
a specific app with --celery myapp.celery:app, or skip discovery with --no-celery.
It reads the framework's real router — routes built in loops, DRF routers, mixins, blueprints,
and FastAPI dependency trees are all found, because the app already resolved them at startup.
Auth is read from Django permissions/mixins/login_required, Flask auth decorators
(flask_login, flask_jwt_extended, …), and FastAPI security schemes and Depends(...).
Models come from Django's registry, your SQLAlchemy mapped classes (Flask/FastAPI), and
Pydantic request/response schemas (FastAPI) — so a field going optional or disappearing from an
API body shows up in the diff. Celery background jobs (tasks + beat schedules) are discovered too,
so a new nightly job — or a task that starts calling Stripe — is caught the same way a route change is.
Coverage will be partial. That's reported, not hidden:
wrote contract.json: 9 routes, 6 models, 0 jobs, auth known on 5/9
Level 2: declare what matters
from lenscheck_contract import contract
@contract.route("POST /orders", auth="user")
@contract.effects("net:api.stripe.com")
def create_order(request):
...
Eight decorators total, max. AI writes these correctly first try, because they look like every Django decorator it has ever seen.
Level 3: enforce it
Install the request hook for your framework (so every view is watched, not just decorated ones), then turn the guard on:
# Django — settings.py
MIDDLEWARE = ["lenscheck_contract.middleware.ContractMiddleware", ...]
# Flask
from lenscheck_contract import install_flask
install_flask(app)
# FastAPI
from lenscheck_contract import install_fastapi
install_fastapi(app)
# then, anywhere at startup (conftest.py, apps.py, main.py):
from lenscheck_contract import guard
guard.install(mode="error") # off | record | warn | error
Now an undeclared call fails loudly:
shop.views.leaky_order performed undeclared effect 'net:api.stripe.com'.
Declared: none. Add @contract.effects('net:api.stripe.com') or remove the call.
This is the part no linter can do. The bad code doesn't ship.
Don't know what to declare? Run your tests in record mode and let it tell you:
guard.install(mode="record")
# ... run test suite ...
json.dump(guard.suggestions(), open("observed.json", "w"))
lenscheck-contract suggest observed.json # prints the decorators to paste in
In CI — the GitHub Action (one step)
Drop this into a pull_request workflow. It exports the contract on the PR head and base, posts a
sticky comment with what changed, and fails the build at your chosen severity.
name: contract
on: pull_request
permissions: { contents: read, pull-requests: write }
jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # base + head both needed
- uses: AnkushSinghGandhi/lenscheck-contract@v1
with:
settings: myproject.settings # Django …
# app: myapp.main:app # … or Flask/FastAPI (omit `settings`)
install: pip install -r requirements.txt
fail-on: risky
| Input | Default | What |
|---|---|---|
settings |
— | DJANGO_SETTINGS_MODULE (Django). Omit for Flask/FastAPI. |
app |
— | Flask/FastAPI app as module:attr (or a create_app factory). |
install |
— | Command to install your app's deps before probing. |
fail-on |
risky |
never | review | risky | any. |
comment |
true |
Post the diff as a sticky PR comment. |
root / python-version |
. / 3.11 |
sys.path root · Python version. |
Prefer raw steps? The Action just wraps these:
- run: lenscheck-contract export --settings myproject.settings -o head.json
- run: git checkout ${{ github.base_ref }}
- run: lenscheck-contract export --settings myproject.settings -o base.json
- run: lenscheck-contract diff base.json head.json --markdown --fail-on risky
--fail-on risky exits 1 on auth weakening, new outside calls, dropped fields,
relaxed uniqueness, or changed relations.
How the effect guard works
One hook at the socket layer, not per-library. requests, httpx, urllib,
boto3, stripe — all covered by the same code, including libraries that
don't exist yet.
socket.getaddrinfois checked before it runs, so a blocked call never leaves the processsocket.socket.connectcatches direct-IP connections- the event loop's
getaddrinfois hooked too, so async clients (httpx.AsyncClient,aiohttp) are caught in the endpoint's own context — not lost in a resolver thread smtplib.SMTP.sendmailcatches email- localhost and unix sockets are never effects, so your database doesn't trip it
Async caveat: uvloop and aiodns resolve outside Python's
socketmodule entirely, so the guard can't see those. Standardasyncio(the uvicorn default) is covered.
Patterns support wildcards: net:*.stripe.com, net:*, email.
What it does not catch
Worth being blunt about.
- Business logic. If AI changes a discount from 10% to 90%, the contract is identical. Routes same, models same, effects same. Tests catch that; this doesn't.
- Anything the router never sees. Dead code, unmounted views.
- Dynamic hostnames are caught at runtime, not at export time. The contract records what you declared; the guard records what actually happened.
Structural mistakes: this. Logic mistakes: your tests. You need both.
Try it
git clone ... && cd lenscheck-contract
pip install -e ".[dev]"
pytest
cd examples/demo && lenscheck-contract export --settings settings --root . -o /tmp/base.json
The money shot — one bad PR, all three frameworks
Each runs the same "add order analytics" PR that sneaks in three dangerous changes, and shows
lenscheck-contract catching all three (two in CI, one at runtime):
bash examples/three_bad_things/run.sh # Django
bash examples/flask/run.sh # Flask (routes + SQLAlchemy models)
bash examples/fastapi/run.sh # FastAPI (dependency auth + Pydantic models + async guard)
Contributing
This is the MIT part of Lenscheck — PRs welcome, no CLA needed. New framework probes, ORM support, and examples are great first contributions. Report a bug and get your name + GitHub on the Wall of Fame at lenscheck.dev; merged fixes earn stickers. See CONTRIBUTING.md.
MIT.
Metadata
Release files for lenscheck-contract 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| lenscheck_contract-1.3.0.tar.gz | 39.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lenscheck_contract-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.0 kB
Release files / lenscheck_contract-1.3.0.tar.gz
| Download URL | lenscheck_contract-1.3.0.tar.gz |
|---|---|
| Size | 39.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7f5c0dbda014fc3e09bf6bc3913c63f89e599fe7466cca99e8eb75f97f02b788
|
|
BLAKE2b-256 checksum How to use checksums |
f7533d5a4bd29cc5dd778c60e4877aa94418b806e908ae2f1be91043c80327d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 22, 2026.
Transparency logRelease files / lenscheck_contract-1.3.0-py3-none-any.whl
| Download URL | lenscheck_contract-1.3.0-py3-none-any.whl |
|---|---|
| Size | 34.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0687a26b49155e851b8f1fe741413b7ddd5e784b1745b2dd5832213ff72f0ed1
|
|
BLAKE2b-256 checksum How to use checksums |
0ebc850d39f937205a0d58db94c6a18ba9fc61a6c8ffd25ff8a746c7f46966e2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 22, 2026.
Transparency logSecurity for lenscheck-contract 1.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
All 2 files in this release have been uploaded with provenance metadata.
Attestation for lenscheck_contract-1.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 Sep 22, 2026.
Transparency logA 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.