BHUVANSH855/pyrift

Detect silent Python behaviour differences across CPython versions and CPython vs PyPy

11

stars

119

commits

Python

primary language

Sep 3, 2026

updated

pypi.org/project/pyrift/
cpython
developer-tools
linter
pypy
python
static-analysis
Browse cluster: Static analysis and linting tools

README

Quick start - Python API

import pyrift

# Scan a directory
result = pyrift.scan("./src")

print(result)
# ScanResult(files=23, errors=2, warnings=1, score=77)

# Iterate findings
for finding in result.findings:
    print(finding)

# Filter by severity
for error in result.errors:
    print(f"{error.file}:{error.line} - {error.title}")

# Export formats
json_output     = pyrift.to_json(result)
markdown_output = pyrift.to_markdown(result)
text_output     = pyrift.to_text(result)

# Scan a single file
findings = pyrift.scan_file("./src/utils.py")

Rules

CPython rules - version compatibility

Rule IDTitleRuntimeStatus
CPY001Dict ordering assumption — comparing dict view to ordered sequenceCPythonActive
CPY002Exception.add_note() requires Python 3.11+CPythonActive
CPY003XY union type syntax requires Python 3.10+CPython
CPY004tomllib requires Python 3.11+CPythonActive
CPY005match/case requires Python 3.10+CPythonActive
CPY006asyncio.timeout() / TaskGroup requires Python 3.11+CPythonActive
CPY007Module removed in Python 3.13CPythonActive
CPY008slots may not prevent dict on Python < 3.10CPythonActive
CPY009ExceptionGroup requires Python 3.11+CPythonActive
CPY010@dataclass(slots=True) requires Python 3.10+CPythonActive
CPY011typing.Self requires Python 3.11+CPythonActive
CPY012typing.LiteralString requires Python 3.11+CPythonActive
CPY013typing.override requires Python 3.12+CPythonActive
CPY014typing.TypeAlias requires Python 3.10+CPythonActive
CPY015typing.Never requires Python 3.11+CPythonActive
CPY016typing.TypeVarTuple requires Python 3.11+CPythonActive
CPY017typing.Unpack requires Python 3.11+CPythonActive
CPY018typing.Required / NotRequired requires Python 3.11+CPythonActive
CPY019distutils removed in Python 3.12+CPythonActive
CPY020datetime.UTC requires Python 3.11+CPythonActive
CPY021asyncio.iscoroutinefunction() deprecated since 3.12CPythonActive
CPY022Bitwise inversion on bool (~True/~False) deprecated in 3.12CPythonActive
CPY023multiprocessing default start method changing in Python 3.14CPythonActive
CPY024typing.TypeGuard requires Python 3.10+CPythonActive
CPY025typing.ParamSpec requires Python 3.10+CPythonActive
CPY026typing.io and typing.re removed in Python 3.13CPythonActive
CPY027locale.resetlocale() removed in Python 3.13CPythonActive
CPY028lib2to3 removed in Python 3.13CPythonActive
CPY029locals() semantics changed in Python 3.13 (PEP 667)CPythonActive
CPY030sys.path no longer accepts bytes entries in Python 3.11+CPythonActive
CPY031typing.assert_never requires Python 3.11+CPythonActive
CPY032typing.reveal_type requires Python 3.11+CPythonActive
CPY033pathlib.Path.is_relative_to() requires Python 3.9+CPythonActive
CPY034int.bit_count() requires Python 3.10+CPythonActive
CPY035str.removeprefix/removesuffix requires Python 3.9+CPythonActive
CPY036datetime.utcnow() deprecated since Python 3.12CPythonActive
CPY037datetime.utcfromtimestamp() deprecated since Python 3.12CPythonActive
CPY038asyncio.get_event_loop() raises RuntimeError in Python 3.14+CPythonActive
CPY039zoneinfo module requires Python 3.9+CPythonActive
CPY040graphlib module requires Python 3.9+CPythonActive
CPY041dictmerge operator requires Python 3.9+CPython
CPY042aiter() and anext() builtins require Python 3.10+CPythonActive
CPY043math.lcm() requires Python 3.9+CPythonActive
CPY044math.gcd() with multiple args requires Python 3.9+CPythonActive
CPY045NaN hash behaviour changed in Python 3.10CPythonActive
CPY046open() without encoding= uses platform-dependent encoding before 3.15CPythonActive
CPY047collections.abc.ByteString removed in Python 3.15CPythonActive
CPY048concurrent.interpreters requires Python 3.14+CPythonActive
CPY049compression.zstd requires Python 3.14+CPythonActive
CPY050PurePath.is_reserved() deprecated in 3.13, removed in 3.15CPythonActive
CPY051Unsynchronized module-level mutable state may be unsafe in free-threaded PythonCPythonActive
CPY053typing.get_overloads() requires Python 3.11+CPythonActive
CPY054int() no longer delegates to trunc() in Python 3.14CPythonActive
CPY055NotImplemented in boolean context raises TypeError in Python 3.14CPythonActive
CPY057pickle default protocol changed to 5 in Python 3.14CPythonActive
CPY062string.templatelib requires Python 3.14+CPythonActive
CPY063annotationlib requires Python 3.14+CPythonActive

PyPy rules - runtime differences

Rule IDTitleRuntimeStatus
PPY001Relying on del for resource cleanup breaks on PyPyPyPyActive
PPY002ctypes usage may silently fail on PyPyPyPyActive
PPY003sys.getrefcount() is meaningless on PyPyPyPyActive
PPY004weakref.proxy() lifetime differs on PyPy due to GC modelPyPyActive
PPY005File write without explicit lifecycle management on PyPyPyPyActive
PPY006Monkey-patching built-in types behaves differently on PyPyPyPyActive
PPY007sys.intern() identity guarantees differ on PyPyPyPyActive
PPY008threading.local() cleanup timing differs on PyPyPyPyActive
PPY009id() stability depends on PyPy GC configurationPyPyActive
PPY010gc.collect() behaviour differs on PyPyPyPyActive
PPY012Overriding built-in methods may behave differently on PyPyPyPyActive
PPY013sys.getsizeof() raises TypeError on PyPyPyPyActive
PPY014String concatenation in loop is O(n²) on PyPyPyPyActive
PPY015Generator cleanup timing differs on PyPyPyPyActive
PPY016Instance dict order-sensitive access may differ on PyPyPyPyActive
PPY017Adding del to existing class not called on PyPyPyPyActive
PPY018sys.setrecursionlimit() behaviour differs on PyPyPyPyActive
PPY019float('nan') identity differs between CPython and PyPyPyPyActive
PPY021Socket not closed promptly on PyPy — GC timingPyPyActive
PPY022PYTHONHASHSEED=0 has no effect on PyPy hash randomisationPyPyActive
PPY023inspect.ismethod() returns different results on PyPyPyPyActive
PPY024timeit reports average not minimum on PyPyPyPyActive
PPY025Set iteration order differs between CPython and PyPyPyPyActive
PPY026builtins is always a module on PyPy, never a dictPyPyActive
PPY027Deleting module/class attributes may be slower on PyPyPyPyActive
PPY028readline.parse_and_bind() silently ignored on PyPyPyPyActive
PPY029Assigning to builtins has no effect on PyPyPyPyActive
PPY030sys.flags values may differ between CPython and PyPyPyPyActive
PPY031Integer 'is' identity semantics differ on PyPyPyPyActive
PPY032Mutating dict keys raises RuntimeError on PyPyPyPyActive
PPY033Exceptions in del appear at unpredictable times on PyPyPyPyActive
PPY034hash() values may differ between CPython and PyPyPyPyActive
PPY035C extension packages may not work correctly on PyPyPyPyActive
PPY036open() line buffering behaves differently on PyPyPyPyActive
PPY037os.urandom() source may differ on PyPyPyPyActive
PPY038decimal module uses different backend on PyPyPyPyActive
PPY039os.fork() may not work correctly on all PyPy platformsPyPyActive
PPY040subprocess.PIPE buffering may cause deadlocks on PyPyPyPyActive
PPY041dictoperator requires PyPy 7.3.7+ (Python 3.9 compat)PyPy
PPY042print(flush=True) may not flush immediately on PyPyPyPyActive
PPY044Exception variable cleanup timing differs on PyPyPyPyActive
PPY045sys.settrace() disables JIT and is unreliable on PyPyPyPyActive
PPY047ctypes.util.find_library() unreliable on PyPyPyPyActive

Cross-runtime rules

Rule IDTitleRuntimeStatus
PPY011array.array('u') type code removed in Python 3.13BothActive

Full rule documentation: docs/rules.md


Use in CI

Add pyrift to your GitHub Actions workflow:

- name: Run pyrift
  run: |
    pip install pyrift
    pyrift scan . --format json --output pyrift-report.json
    pyrift scan .

Exit code is 1 when errors are found - fails the CI build automatically. Use --exit-zero to report without failing.


Use with pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/BHUVANSH855/pyrift
    rev: v0.8.0
    hooks:
      - id: pyrift

Why pyrift?

ToolWhat it catchesWhat it misses
pylint / ruffStyle, common bugsRuntime behaviour differences
mypy / pyrightType errorsRuntime behaviour differences
pip-auditKnown CVEsBehaviour differences
banditSecurity patternsBehaviour differences
pyriftSilent runtime behaviour differences(that's the whole point)

PyRift focuses on compatibility and behavioural differences that conventional linters and type checkers generally do not model. It complements — not replaces — the tools above.


Git-aware scanning

For maintainer and CI workflows, PyRift can scan only Python files changed relative to a Git revision (including staged and untracked changes):

pyrift scan . --changed-only
pyrift scan . --changed-only --base origin/main

Features

Dynamic import detection

Some compatibility issues hide behind imports resolved at runtime rather than through import statements. PyRift detects these too:

  • importlib.import_module("removed_module")
  • __import__("removed_module")

Only statically-resolvable module names are flagged; names computed at runtime from a variable are deliberately left alone to avoid false positives.

Version-guard awareness

PyRift understands sys.version_info guards. A module import protected by a sys.version_info >= (3, N) check that already covers the required version is not reported, because the guarded code never runs on an affected interpreter.

Confidence and evidence

Every finding carries a confidence (high / medium / low) and an evidence_type (official_docs, runtime_probe, deprecation_warn, pep, observed, inferred). These are assigned from a central reviewed table (pyrift/rule_metadata.py); unreviewed rules conservatively default to low / inferred rather than over-claiming certainty.

Multi-name import deduplication

from tomllib import load, loads produces a single finding, not one per imported name, so reports stay readable and stable.

Target-aware filtering

When pyproject.toml declares requires-python, PyRift drops CPython findings that cannot affect the project's supported version range. You can also override with --python-min / --python-max.

Rule-robustness guarantee

Every rule is exercised against a broad suite of exotic-but-valid AST constructs via benchmark/fuzz_harness.py to guarantee no rule ever crashes on valid Python — regardless of finding outcome.


Roadmap

Planned for upcoming versions - contributions welcome:

  • CPY064+ - next CPython compatibility rules (open for contributions)
  • PPY048+ - next PyPy runtime difference rules (open for contributions)
  • Pre-commit hook native support
  • VS Code extension
  • GitHub Action marketplace listing

See CONTRIBUTING.md to add a rule yourself. New rule IDs are assigned after reviewing the existing rule inventory to avoid collisions and duplicate coverage.


Contributing

Contributions are very welcome - especially new rules for behaviour differences you have personally encountered.

See CONTRIBUTING.md for the full guide.


Project status

  • Version: 0.8.0
  • Rules: 101 total (57 CPython + 43 PyPy + 1 cross-runtime)
  • Tests: 1117 passing
  • Dependencies: zero
  • Python: 3.10+

Rule Trustworthiness

Every finding carries a confidence level and evidence type. Rules are classified into three tiers:

TierConfidenceEvidenceIntent basisDescription
AHighofficial_docs, pep, deprecation_warndocumented / deprecationAuthoritative compatibility evidence; supports claims about documented changes.
BHighruntime_probeobserved unless separately documentedRuntime-verified behavior; confirms the difference but does not by itself prove maintainer intent.
CMedium / Lowobserved, inferredobserved / inferredEmpirical or inferred behavior needing independent verification.

Rule evidence and intentionality are documented in docs/behavior-evidence.md.

Unreviewed rules (no entry in pyrift/rule_metadata.py) default to low confidence, inferred evidence, and inferred intent basis. See the Confidence and evidence feature description for details.


Known Limitations

  • Static analysis cannot verify runtime behaviour. PyRift inspects ASTs, not executed code. Some findings may be false positives if the flagged code is never reached or is guarded at runtime.
  • Some rules are heuristics, not proofs. A rule may flag code that happens to be compatible in practice. Always review findings in context.
  • PyPy rules may become outdated as PyPy evolves. Report false positives so rules can be updated or deprecated.
  • Free-threading rules are experimental. The CPython 3.13+ free-threading (no-GIL) build is new and its semantics are still stabilising. Rules for free-threaded code may change.
  • Version ranges are conservative. Affected-version bounds are based on documented changes; edge cases or backported fixes may alter the actual impact.

Author

Built by Bhuvansh Kataria - CPython contributor and PyPy toolkit author.


License

MIT - see LICENSE

Security

Found a security issue in PyRift itself? Please follow our Security Policy and report it privately rather than opening a public issue.

Contributors

BHUVANSH855

117 commits

BHUVANSH855/pyrift

Detect silent Python behaviour differences across CPython versions and CPython vs PyPy

11

stars

119

commits

Python

primary language

Sep 3, 2026

updated

pypi.org/project/pyrift/
cpython
developer-tools
linter
pypy
python
static-analysis
Browse cluster: Static analysis and linting tools

README

Quick start - Python API

import pyrift

# Scan a directory
result = pyrift.scan("./src")

print(result)
# ScanResult(files=23, errors=2, warnings=1, score=77)

# Iterate findings
for finding in result.findings:
    print(finding)

# Filter by severity
for error in result.errors:
    print(f"{error.file}:{error.line} - {error.title}")

# Export formats
json_output     = pyrift.to_json(result)
markdown_output = pyrift.to_markdown(result)
text_output     = pyrift.to_text(result)

# Scan a single file
findings = pyrift.scan_file("./src/utils.py")

Rules

CPython rules - version compatibility

Rule IDTitleRuntimeStatus
CPY001Dict ordering assumption — comparing dict view to ordered sequenceCPythonActive
CPY002Exception.add_note() requires Python 3.11+CPythonActive
CPY003XY union type syntax requires Python 3.10+CPython
CPY004tomllib requires Python 3.11+CPythonActive
CPY005match/case requires Python 3.10+CPythonActive
CPY006asyncio.timeout() / TaskGroup requires Python 3.11+CPythonActive
CPY007Module removed in Python 3.13CPythonActive
CPY008slots may not prevent dict on Python < 3.10CPythonActive
CPY009ExceptionGroup requires Python 3.11+CPythonActive
CPY010@dataclass(slots=True) requires Python 3.10+CPythonActive
CPY011typing.Self requires Python 3.11+CPythonActive
CPY012typing.LiteralString requires Python 3.11+CPythonActive
CPY013typing.override requires Python 3.12+CPythonActive
CPY014typing.TypeAlias requires Python 3.10+CPythonActive
CPY015typing.Never requires Python 3.11+CPythonActive
CPY016typing.TypeVarTuple requires Python 3.11+CPythonActive
CPY017typing.Unpack requires Python 3.11+CPythonActive
CPY018typing.Required / NotRequired requires Python 3.11+CPythonActive
CPY019distutils removed in Python 3.12+CPythonActive
CPY020datetime.UTC requires Python 3.11+CPythonActive
CPY021asyncio.iscoroutinefunction() deprecated since 3.12CPythonActive
CPY022Bitwise inversion on bool (~True/~False) deprecated in 3.12CPythonActive
CPY023multiprocessing default start method changing in Python 3.14CPythonActive
CPY024typing.TypeGuard requires Python 3.10+CPythonActive
CPY025typing.ParamSpec requires Python 3.10+CPythonActive
CPY026typing.io and typing.re removed in Python 3.13CPythonActive
CPY027locale.resetlocale() removed in Python 3.13CPythonActive
CPY028lib2to3 removed in Python 3.13CPythonActive
CPY029locals() semantics changed in Python 3.13 (PEP 667)CPythonActive
CPY030sys.path no longer accepts bytes entries in Python 3.11+CPythonActive
CPY031typing.assert_never requires Python 3.11+CPythonActive
CPY032typing.reveal_type requires Python 3.11+CPythonActive
CPY033pathlib.Path.is_relative_to() requires Python 3.9+CPythonActive
CPY034int.bit_count() requires Python 3.10+CPythonActive
CPY035str.removeprefix/removesuffix requires Python 3.9+CPythonActive
CPY036datetime.utcnow() deprecated since Python 3.12CPythonActive
CPY037datetime.utcfromtimestamp() deprecated since Python 3.12CPythonActive
CPY038asyncio.get_event_loop() raises RuntimeError in Python 3.14+CPythonActive
CPY039zoneinfo module requires Python 3.9+CPythonActive
CPY040graphlib module requires Python 3.9+CPythonActive
CPY041dictmerge operator requires Python 3.9+CPython
CPY042aiter() and anext() builtins require Python 3.10+CPythonActive
CPY043math.lcm() requires Python 3.9+CPythonActive
CPY044math.gcd() with multiple args requires Python 3.9+CPythonActive
CPY045NaN hash behaviour changed in Python 3.10CPythonActive
CPY046open() without encoding= uses platform-dependent encoding before 3.15CPythonActive
CPY047collections.abc.ByteString removed in Python 3.15CPythonActive
CPY048concurrent.interpreters requires Python 3.14+CPythonActive
CPY049compression.zstd requires Python 3.14+CPythonActive
CPY050PurePath.is_reserved() deprecated in 3.13, removed in 3.15CPythonActive
CPY051Unsynchronized module-level mutable state may be unsafe in free-threaded PythonCPythonActive
CPY053typing.get_overloads() requires Python 3.11+CPythonActive
CPY054int() no longer delegates to trunc() in Python 3.14CPythonActive
CPY055NotImplemented in boolean context raises TypeError in Python 3.14CPythonActive
CPY057pickle default protocol changed to 5 in Python 3.14CPythonActive
CPY062string.templatelib requires Python 3.14+CPythonActive
CPY063annotationlib requires Python 3.14+CPythonActive

PyPy rules - runtime differences

Rule IDTitleRuntimeStatus
PPY001Relying on del for resource cleanup breaks on PyPyPyPyActive
PPY002ctypes usage may silently fail on PyPyPyPyActive
PPY003sys.getrefcount() is meaningless on PyPyPyPyActive
PPY004weakref.proxy() lifetime differs on PyPy due to GC modelPyPyActive
PPY005File write without explicit lifecycle management on PyPyPyPyActive
PPY006Monkey-patching built-in types behaves differently on PyPyPyPyActive
PPY007sys.intern() identity guarantees differ on PyPyPyPyActive
PPY008threading.local() cleanup timing differs on PyPyPyPyActive
PPY009id() stability depends on PyPy GC configurationPyPyActive
PPY010gc.collect() behaviour differs on PyPyPyPyActive
PPY012Overriding built-in methods may behave differently on PyPyPyPyActive
PPY013sys.getsizeof() raises TypeError on PyPyPyPyActive
PPY014String concatenation in loop is O(n²) on PyPyPyPyActive
PPY015Generator cleanup timing differs on PyPyPyPyActive
PPY016Instance dict order-sensitive access may differ on PyPyPyPyActive
PPY017Adding del to existing class not called on PyPyPyPyActive
PPY018sys.setrecursionlimit() behaviour differs on PyPyPyPyActive
PPY019float('nan') identity differs between CPython and PyPyPyPyActive
PPY021Socket not closed promptly on PyPy — GC timingPyPyActive
PPY022PYTHONHASHSEED=0 has no effect on PyPy hash randomisationPyPyActive
PPY023inspect.ismethod() returns different results on PyPyPyPyActive
PPY024timeit reports average not minimum on PyPyPyPyActive
PPY025Set iteration order differs between CPython and PyPyPyPyActive
PPY026builtins is always a module on PyPy, never a dictPyPyActive
PPY027Deleting module/class attributes may be slower on PyPyPyPyActive
PPY028readline.parse_and_bind() silently ignored on PyPyPyPyActive
PPY029Assigning to builtins has no effect on PyPyPyPyActive
PPY030sys.flags values may differ between CPython and PyPyPyPyActive
PPY031Integer 'is' identity semantics differ on PyPyPyPyActive
PPY032Mutating dict keys raises RuntimeError on PyPyPyPyActive
PPY033Exceptions in del appear at unpredictable times on PyPyPyPyActive
PPY034hash() values may differ between CPython and PyPyPyPyActive
PPY035C extension packages may not work correctly on PyPyPyPyActive
PPY036open() line buffering behaves differently on PyPyPyPyActive
PPY037os.urandom() source may differ on PyPyPyPyActive
PPY038decimal module uses different backend on PyPyPyPyActive
PPY039os.fork() may not work correctly on all PyPy platformsPyPyActive
PPY040subprocess.PIPE buffering may cause deadlocks on PyPyPyPyActive
PPY041dictoperator requires PyPy 7.3.7+ (Python 3.9 compat)PyPy
PPY042print(flush=True) may not flush immediately on PyPyPyPyActive
PPY044Exception variable cleanup timing differs on PyPyPyPyActive
PPY045sys.settrace() disables JIT and is unreliable on PyPyPyPyActive
PPY047ctypes.util.find_library() unreliable on PyPyPyPyActive

Cross-runtime rules

Rule IDTitleRuntimeStatus
PPY011array.array('u') type code removed in Python 3.13BothActive

Full rule documentation: docs/rules.md


Use in CI

Add pyrift to your GitHub Actions workflow:

- name: Run pyrift
  run: |
    pip install pyrift
    pyrift scan . --format json --output pyrift-report.json
    pyrift scan .

Exit code is 1 when errors are found - fails the CI build automatically. Use --exit-zero to report without failing.


Use with pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/BHUVANSH855/pyrift
    rev: v0.8.0
    hooks:
      - id: pyrift

Why pyrift?

ToolWhat it catchesWhat it misses
pylint / ruffStyle, common bugsRuntime behaviour differences
mypy / pyrightType errorsRuntime behaviour differences
pip-auditKnown CVEsBehaviour differences
banditSecurity patternsBehaviour differences
pyriftSilent runtime behaviour differences(that's the whole point)

PyRift focuses on compatibility and behavioural differences that conventional linters and type checkers generally do not model. It complements — not replaces — the tools above.


Git-aware scanning

For maintainer and CI workflows, PyRift can scan only Python files changed relative to a Git revision (including staged and untracked changes):

pyrift scan . --changed-only
pyrift scan . --changed-only --base origin/main

Features

Dynamic import detection

Some compatibility issues hide behind imports resolved at runtime rather than through import statements. PyRift detects these too:

  • importlib.import_module("removed_module")
  • __import__("removed_module")

Only statically-resolvable module names are flagged; names computed at runtime from a variable are deliberately left alone to avoid false positives.

Version-guard awareness

PyRift understands sys.version_info guards. A module import protected by a sys.version_info >= (3, N) check that already covers the required version is not reported, because the guarded code never runs on an affected interpreter.

Confidence and evidence

Every finding carries a confidence (high / medium / low) and an evidence_type (official_docs, runtime_probe, deprecation_warn, pep, observed, inferred). These are assigned from a central reviewed table (pyrift/rule_metadata.py); unreviewed rules conservatively default to low / inferred rather than over-claiming certainty.

Multi-name import deduplication

from tomllib import load, loads produces a single finding, not one per imported name, so reports stay readable and stable.

Target-aware filtering

When pyproject.toml declares requires-python, PyRift drops CPython findings that cannot affect the project's supported version range. You can also override with --python-min / --python-max.

Rule-robustness guarantee

Every rule is exercised against a broad suite of exotic-but-valid AST constructs via benchmark/fuzz_harness.py to guarantee no rule ever crashes on valid Python — regardless of finding outcome.


Roadmap

Planned for upcoming versions - contributions welcome:

  • CPY064+ - next CPython compatibility rules (open for contributions)
  • PPY048+ - next PyPy runtime difference rules (open for contributions)
  • Pre-commit hook native support
  • VS Code extension
  • GitHub Action marketplace listing

See CONTRIBUTING.md to add a rule yourself. New rule IDs are assigned after reviewing the existing rule inventory to avoid collisions and duplicate coverage.


Contributing

Contributions are very welcome - especially new rules for behaviour differences you have personally encountered.

See CONTRIBUTING.md for the full guide.


Project status

  • Version: 0.8.0
  • Rules: 101 total (57 CPython + 43 PyPy + 1 cross-runtime)
  • Tests: 1117 passing
  • Dependencies: zero
  • Python: 3.10+

Rule Trustworthiness

Every finding carries a confidence level and evidence type. Rules are classified into three tiers:

TierConfidenceEvidenceIntent basisDescription
AHighofficial_docs, pep, deprecation_warndocumented / deprecationAuthoritative compatibility evidence; supports claims about documented changes.
BHighruntime_probeobserved unless separately documentedRuntime-verified behavior; confirms the difference but does not by itself prove maintainer intent.
CMedium / Lowobserved, inferredobserved / inferredEmpirical or inferred behavior needing independent verification.

Rule evidence and intentionality are documented in docs/behavior-evidence.md.

Unreviewed rules (no entry in pyrift/rule_metadata.py) default to low confidence, inferred evidence, and inferred intent basis. See the Confidence and evidence feature description for details.


Known Limitations

  • Static analysis cannot verify runtime behaviour. PyRift inspects ASTs, not executed code. Some findings may be false positives if the flagged code is never reached or is guarded at runtime.
  • Some rules are heuristics, not proofs. A rule may flag code that happens to be compatible in practice. Always review findings in context.
  • PyPy rules may become outdated as PyPy evolves. Report false positives so rules can be updated or deprecated.
  • Free-threading rules are experimental. The CPython 3.13+ free-threading (no-GIL) build is new and its semantics are still stabilising. Rules for free-threaded code may change.
  • Version ranges are conservative. Affected-version bounds are based on documented changes; edge cases or backported fixes may alter the actual impact.

Author

Built by Bhuvansh Kataria - CPython contributor and PyPy toolkit author.


License

MIT - see LICENSE

Security

Found a security issue in PyRift itself? Please follow our Security Policy and report it privately rather than opening a public issue.

Contributors

BHUVANSH855

117 commits

Languages

Python

100.0%