The problem
Every Python developer has hit a bug that:
- Passes all linting checks
- Passes all type checking
- Only appears at runtime, often in production
These are not syntax errors. They are not CVEs. They are silent behaviour differences — code that runs without errors but produces wrong results, leaks resources, or crashes only in certain environments.
I got frustrated with this pattern while working on CPython contributions and maintaining a PyPy review toolkit. So I built pyrift.
What pyrift does
pip install pyrift
pyrift scan .
It statically analyses your Python code and flags patterns that behave differently across CPython versions or between CPython and PyPy.
Example output
[ERROR] src/server.py:42 CPY002: Exception.add_note() requires Python 3.11+
→ Guard with: if sys.version_info >= (3, 11): e.add_note(...)
[ERROR] src/compat.py:17 CPY007: Module removed in Python 3.13
→ Find a third-party replacement for 'cgi' on PyPI
[WARNING] src/resource.py:88 PPY001: Relying on del for resource cleanup breaks on PyPy
→ Use context managers (with statement) or try/finally blocks
Scanned 23 file(s). Found 2 error(s), 1 warning(s). Score: 77/100
Real examples of what it catches
CPython version differences
Exception.add_note() — silent AttributeError on Python 3.10
try:
connect()
except ConnectionError as e:
e.add_note("Check your network") # AttributeError on 3.10
raise
add_note() was added in Python 3.11 (PEP 678). On 3.10 it raises AttributeError inside the except block — the worst possible place to get an error.
asyncio.TaskGroup — AttributeError on Python 3.10
async with asyncio.TaskGroup() as tg: # AttributeError on 3.10
tg.create_task(fetch())
TaskGroup was added in Python 3.11. This crashes silently on 3.10 with no hint of why.
21 stdlib modules removed in Python 3.13
import cgi # ModuleNotFoundError on Python 3.13
import telnetlib # ModuleNotFoundError on Python 3.13
import asynchat # ModuleNotFoundError on Python 3.13
PEP 594 removed 21 legacy modules. pyrift flags all of them.
PyPy runtime differences
__del__ cleanup silently leaks on PyPy
class DatabaseConnection:
def __del__(self):
self.conn.close() # may never run on PyPy
CPython uses reference counting — __del__ runs immediately when the last reference drops. PyPy uses a tracing GC — __del__ may run much later or never. This silently leaks database connections, file handles, and locks.
sys.getrefcount() returns a dummy value on PyPy
if sys.getrefcount(obj) == 1:
cleanup(obj) # always wrong on PyPy
PyPy has no reference counting. sys.getrefcount() always returns a dummy constant. Any logic based on it silently produces wrong results.
Why existing tools miss these
| Tool | What it catches | What it misses |
|---|---|---|
pylint / ruff
|
Style, common bugs | Runtime behaviour differences |
mypy / pyright
|
Type errors | Runtime behaviour differences |
pip-audit |
Known CVEs | Behaviour differences |
bandit |
Security patterns | Behaviour differences |
| pyrift | Silent runtime behaviour differences | (that's the whole point) |
pyrift does not replace any of these tools. It catches what they all miss.
Use it in CI
- name: Run pyrift
run: |
pip install pyrift
pyrift scan .
Exit code is 1 when errors are found — fails the build automatically.
Current state
- 17 rules — 10 CPython version compatibility + 7 PyPy runtime differences
- 70 tests — all passing on Python 3.10, 3.11, 3.12, 3.13
- Zero dependencies — pure Python
- MIT licensed
Try it
pip install pyrift
pyrift scan ./your_project
GitHub: https://github.com/BHUVANSH855/pyrift
PyPI: https://pypi.org/project/pyrift/
Rule docs: https://github.com/BHUVANSH855/pyrift/blob/main/docs/rules.md
Contributions very welcome — especially new rules for behaviour differences you have personally encountered. Rule IDs CPY011+ and PPY008+ are open for community contributions.
Built by a CPython contributor and PyPy toolkit author - Bhuvansh Kataria
Top comments (0)