DEV Community

Bhuvansh
Bhuvansh

Posted on

I built pyrift: a Python tool that catches runtime bugs linters can't see

August 2026 update: pyrift v0.5.0 is now available with 90 rules and 316 tests. pip install --upgrade pyrift

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

TaskGroup was added in Python 3.11. This crashes silently on 3.10 with no hint of why.


datetime.utcnow() — deprecated since Python 3.12

# Bad — deprecated, will be removed
now = datetime.datetime.utcnow()

# Good — timezone-aware
now = datetime.datetime.now(datetime.timezone.utc)
Enter fullscreen mode Exit fullscreen mode

Returns a naive datetime with no timezone info. Deprecated in 3.12, will be removed in a future version. Causes timezone bugs in logging and APIs.


asyncio.get_event_loop() — raises RuntimeError in Python 3.12+

# Bad — RuntimeError in 3.12+ if no loop exists
loop = asyncio.get_event_loop()

# Good
asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Before 3.12 it silently created a new event loop. Now it raises RuntimeError.


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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

CPython uses reference counting — __del__ runs immediately. 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
Enter fullscreen mode Exit fullscreen mode

PyPy has no reference counting. sys.getrefcount() always returns a dummy constant. Any logic based on it silently produces wrong results.


sys.settrace() disables the JIT on PyPy

sys.settrace(my_tracer)  # disables JIT — 10-100x slower on PyPy
Enter fullscreen mode Exit fullscreen mode

Debuggers and coverage tools that use sys.settrace() will silently disable PyPy's JIT compiler, causing massive performance degradation.


sys.getsizeof() raises TypeError on PyPy

size = sys.getsizeof(obj)  # TypeError on PyPy — always
Enter fullscreen mode Exit fullscreen mode

PyPy deliberately raises TypeError because memory sizing by object is meaningless on its GC.


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 .
Enter fullscreen mode Exit fullscreen mode

Exit code is 1 when errors are found — fails the build automatically.


Current state

  • 90 rules — 45 CPython version compatibility + 45 PyPy runtime differences
  • 316 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
Enter fullscreen mode Exit fullscreen mode

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 CPY046+ and PPY046+ are open for community contributions.


Built by a CPython contributor and PyPy toolkit author — Bhuvansh Kataria

Top comments (1)

Collapse
 
bhuvansh855 profile image
Bhuvansh •

Update: pyrift v0.5.0 is now available — biggest release yet.

90 rules total (45 CPython + 45 PyPy), 316 tests, zero dependencies.

New in v0.5.0:

CPython rules added (CPY019–CPY045):

  • datetime.utcnow() and utcfromtimestamp() deprecated in 3.12
  • asyncio.get_event_loop() raises RuntimeError in 3.12+
  • distutils removed in 3.12, lib2to3 removed in 3.13
  • typing.TypeGuard, ParamSpec, TypeAlias, Never, assert_never, reveal_type
  • zoneinfo, graphlib require Python 3.9+
  • int.bit_count(), dict | operator, math.lcm() compatibility rules
  • And more...

PyPy rules added (PPY008–PPY045):

  • sys.getsizeof() raises TypeError on PyPy
  • sys.settrace() disables JIT silently
  • os.fork() unsafe with active JIT
  • String concatenation O(n²) in loops
  • float('nan') identity differs
  • C extension packages compatibility warnings
  • And more...

pip install --upgrade pyrift

Full changelog: github.com/BHUVANSH855/pyrift/blob...
Rule reference: github.com/BHUVANSH855/pyrift/blob...
PyPI: pypi.org/project/pyrift/0.5.0/