PyFix
Python errors, explained and fixed.
PyFix is a safety-first developer tool that helps Python developers — especially beginners — understand and safely fix common Python errors. It now spans two levels of troubleshooting:
- Level 1 — environment, dependency, and import problems (the original V1: missing packages, missing imports, wrong virtual environment, missing files, syntax/indentation errors).
- Level 2 — code-level programming mistakes: wrong function arguments, undefined-variable/keyword-argument typos, unreachable code, statically-provable indexing errors, obvious type mismatches, and attribute typos on builtins/known-safe stdlib modules.
RUN → CAPTURE → LOCATE → CLASSIFY → ANALYZE → EXPLAIN → PROPOSE
→ SHOW DIFF → ASK PERMISSION → BACKUP → APPLY → RERUN → VERIFY
→ ROLLBACK IF NECESSARY
PyFix can automatically diagnose and safely repair certain high-confidence Python problems. More complex bugs may only be diagnosed or explained — PyFix never invents a fix it isn't confident in, and it never pretends a guess is certain.
It is not a reckless auto-fixer. PyFix never installs a package, edits a file, or runs a command without first explaining what's wrong, showing you exactly what it wants to do, and asking permission.
Install
pip install -e .
This registers a pyfix command (see [project.scripts] in
pyproject.toml).
Quick start
pyfix run game.py # run a script; diagnose + fix failures (loops up to 5x, see below)
pyfix run game.py --dry-run # show what PyFix would do, change nothing
pyfix run game.py --yes # auto-approve safe (non-destructive) fixes
pyfix doctor # scan the current project: environment, deps, AND code issues
pyfix analyze file.py # static, no-execution multi-issue report for one file
pyfix analyze file.py --json # same, as machine-readable JSON (for editors/CI)
pyfix explain traceback.txt # explain a saved traceback in plain English
pyfix environment # show the detected Python interpreter/venv
pyfix dependencies # compare requirements.txt vs. what's installed
pyfix diff # show the most recent PyFix-proposed/applied diff
pyfix logs # local diagnostic log of everything PyFix has found/done
pyfix logs --advanced # same, with full per-issue detail
pyfix clear-logs # clear the local diagnostic log (never touches source/backups/Git)
pyfix undo # revert the most recent PyFix edit
--beginner / --advanced (mutually exclusive) work either before or
after the subcommand — pyfix --advanced run file.py and
pyfix run file.py --advanced are equivalent.
What V1 (Level 1) does
The full Detection → Diagnosis → Confidence → Proposed action → Permission → Execution → Verification pipeline, for:
| Category | Behavior |
|---|---|
ModuleNotFoundError / ImportError |
Resolves the import name to a real PyPI package (installed-metadata → verified mapping table → cautious heuristic) and proposes <current-python> -m pip install <package> |
| Package installed in the wrong environment | Detects when the missing package exists under a different interpreter than the one running your program |
NameError from a forgotten import |
AST-based: checks whether the name is used like a module and maps to a real package |
SyntaxError, IndentationError, TabError |
The specific, unambiguous cases (missing block colon, mismatched bracket, unambiguous stray indent — see "Structural Syntax & Formatting Intelligence" below) are auto-fixable; everything else remains explanation-only, with precise file/line location |
FileNotFoundError |
Suggests similarly-named files in the project — never changes a path automatically |
pyfix doctor |
Python version, virtualenv detection, missing imports, syntax validity, requirements.txt, Git status, and now code-level static analysis |
pyfix dependencies |
Compares requirements.txt pins against what's actually installed |
What V2 (Level 2) adds
Built entirely on top of the V1 architecture — no existing detector, safety check, or CLI command was removed or altered in behavior.
| Category | Confidence model | Behavior |
|---|---|---|
NameError — undefined variable typo |
HIGH (single close match) / LOW (multiple candidates) / UNKNOWN (nothing close) | HIGH → proposes a diff-reviewed, line-scoped rename (usernmae → username). LOW → lists candidates, explains only. UNKNOWN → explains only |
TypeError — wrong argument count (missing / too many / duplicate) |
CERTAIN | Explanation-only — PyFix never invents a value for a missing argument or guesses which extra one to drop |
TypeError — unexpected keyword argument |
HIGH (single close match to a real parameter) / explanation-only otherwise | HIGH → proposes a line-scoped rename of just the keyword (taxes= → tax=) |
AttributeError — builtin type typo (list, dict, str, set, tuple, int, float, bytes, frozenset) |
HIGH (one close match) / MEDIUM (a couple) / explanation-only | Always explanation-only by design — PyFix doesn't know every call site, so it reports the correction and lets you apply it |
AttributeError — stdlib module typo (os, pathlib, sys, math, json, re, random, datetime, subprocess, shutil, collections, itertools, typing — see SAFE_INTROSPECTION_MODULES) |
Same as above | Only these modules are ever imported for introspection; anything else (including your own project modules, and third-party packages like requests) is explicitly declined rather than imported |
Unreachable code (after return/raise/break/continue) |
CERTAIN | Static-only (pyfix analyze / doctor) — never fires from a runtime traceback, since unreachable code doesn't raise |
break/continue outside a loop |
CERTAIN | Static-only (in practice Python's compiler already raises SyntaxError for a whole invalid file first; this exists for defensive/partial-file analysis) |
Statically-constant conditions (if 1 == 2:, while True: with no visible exit) |
CERTAIN (constant condition) / LOW (possible infinite loop) | Static-only, explanation-only |
| Out-of-bounds indexing into a literal or single-assignment list/tuple | CERTAIN (literal index) / LOW (dynamic index) | Static-only, explanation-only — a dynamic index is flagged as "possible", never as certain |
Obvious str + int type mismatch |
HIGH | Static-only, explanation-only — PyFix states it can't safely determine whether str(x) or int(x) was intended |
| Wrong argument count, statically (calling a locally-defined function) | CERTAIN | Folded into pyfix analyze / doctor for multi-diagnostic reporting, without needing to actually run the program |
pyfix run's iterative loop (Level-2 §17): each run attempts at
most 5 automatic repair iterations (run → diagnose → propose → ask
→ apply → verify → re-run if a new fixable error appears). It never
loops silently — every iteration's proposal still requires the same
[Y/N] approval (or --yes), and it stops immediately if a fix
can't be auto-applied or if verification fails outright.
pyfix analyze <file> is the new multi-diagnostic entry point:
unlike pyfix run, it never executes your program — it's a pure
static pass that can report several issues from one file at once
(matching the flagship example: a wrong-argument-count call and an
undefined-variable typo, found together, with exact line numbers).
Safety model (unchanged, and extended)
Every repair is still represented internally as a RepairProposal
(pyfix.core.models) carrying its category, explanation, confidence
score, risk level, affected files, exact diff, exact command argv, and
a verification plan. Level-2 static findings use a separate,
richer Diagnostic model (pyfix.diagnostics.models, 5-tier
confidence: CERTAIN/HIGH/MEDIUM/LOW/UNKNOWN) that only becomes an
executable RepairProposal when PyFix is actually confident enough to
act — most Level-2 findings are, by design, explanation-only.
- Never builds shell strings; every command is an argv list run
with
shell=False. - Never guesses a package name, a variable name, or a keyword
argument name with confidence it doesn't have. The new
pyfix.analysis.typo_resolutionmodule is shared by every "did you mean...?" check so the same HIGH/LOW/UNKNOWN rules apply everywhere. - Never renames more than the single, exact occurrence PyFix
diagnosed. The new
pyfix.source.rename_editoris line-scoped and word-boundary matched — it will not touch a same-named identifier on another line, and it will not matchcatinsideconcatenate. - Never imports a module just to inspect its attributes unless
that module is in a small, hardcoded, standard-library-only
allow-list (
AttributeTypoDetector.SAFE_INTROSPECTION_MODULES) — your own project modules and third-party packages are never imported for this purpose, because that would execute untrusted code. - Never edits source with string replacement — insertion/rename
points come from the real AST or precise line/word-boundary
matching, every edit produces a unified diff, and a timestamped
backup is written before the file is touched (undo-able via
pyfix undo). - Never installs into the wrong interpreter — always
<sys.executable> -m pip install .... - Validates every package name and command argv against an allow-list before execution, rejecting shell metacharacters.
- Validates that any file PyFix touches resolves to inside the project root.
- Never claims a fix worked without re-running the program and checking the result.
- Never discards uncommitted Git work.
- Bounds the new iterative repair loop to 5 attempts — no infinite automatic-repair loops.
Project layout
src/pyfix/
├── cli/ # argparse-based CLI (pyfix run/doctor/analyze/explain/...)
├── core/ # RepairProposal data model + the orchestrator pipeline (+ MAX_REPAIR_ITERATIONS)
├── analysis/ # NEW — reusable static analysis: scope, typo resolution, control-flow/
│ # indexing/type-mismatch checks, the multi-diagnostic runner
├── diagnostics/ # NEW — the Diagnostic data model (5-tier confidence), separate from RepairProposal
├── detectors/ # one class per error category — the extensibility seam
│ # + wrong_arguments.py, attribute_typo.py (NEW)
├── packages/ # import-name → PyPI-name resolution
├── environment/ # interpreter/venv/conda detection
├── execution/ # pip install + program re-run, both via safe argv
├── safety/ # input validation + the undo/backup log
├── source/ # AST analysis + diff-producing, backed-up source edits
│ # + rename_editor.py (NEW) — precise, line-scoped identifier renames
├── traceback/ # deterministic traceback parser (no LLM)
├── git/ # read-only git status awareness
└── ui/ # plain-text terminal rendering
tests/
├── level2/ # NEW — Level-2 unit + integration + security tests
├── fixtures/level2/ # NEW — small realistic buggy programs used by the Level-2 tests
└── ... # existing V1 test tree, unmodified in structure
tools/microtest.py # see "Running tests" below
Adding a new error category means writing one class implementing
pyfix.detectors.base.Detector (for a runtime-traceback-triggered
check) or a function added to pyfix.analysis.runner.run_static_analysis
(for a static, no-execution check) — nothing else changes.
Running tests
pip install -e ".[dev]"
pytest
Verification status of this build
This environment has no network access, so real pytest could
not be installed here. Everything below is reported honestly against
that constraint, per the project's own "no fake functionality" rule:
VERIFIED WITH MICROTEST (python3 tools/microtest.py — a small,
dependency-free stand-in implementing just the pytest features this
suite uses: fixtures, tmp_path, monkeypatch, capsys,
parametrize, raises, mark.skip; it runs the actual test files in
tests/ unmodified in logic):
- 85 passed, 0 failed, 1 skipped, covering: V1 detectors and
pipeline (unchanged), safety/validation (including malicious
package names, argv, and paths), the traceback parser, the package
resolver, and — new in this update — Level-2 static analysis on all
ten
tests/fixtures/level2/*.pyprograms, the wrong-arguments and attribute-typo detectors, the full detect→propose→apply→verify→undo pipeline for a variable typo and a keyword typo, and Level-2-specific security tests (rename-editor word-boundary safety, the attribute-introspection allow-list, malicious traceback content). - The one skip is a real
pip install, which needs network access and is intentionally not run automatically.
VERIFIED (manually, through the actual installed pyfix CLI
against disposable temp projects, not against a real project):
- The flagship Level-2 success-criterion program (wrong-argument-count
call + undefined-variable typo in the same file) —
pyfix analyzecorrectly reports both issues with exact line numbers, in one pass, without executing the program. pyfix run --dry-runon the same program stops at the first runtime exception (the missing-argumentTypeError) and explicitly states it will not guess a value — makes zero changes.- The keyword-argument-typo case (
taxes=→tax=) — proposed, diffed, applied with--yes, and verified by re-running, in a single clean pass. - The builtin attribute-typo case (
list.apend→.append()) and the stdlib module attribute-typo case (math.sqrtt) — both correctly diagnosed and explanation-only. - Confirmed the attribute-typo detector does not attempt to
introspect
requests(or any other non-allow-listed module) even when the AttributeError message looks identical in shape. - All pre-existing V1 flows re-run and confirmed unaffected: the
pygame missing-package flow,
pyfix doctor(now additionally showing a "Code (static analysis)" section),--helplisting every original command plus the newanalyzecommand, and the argparse flag-ordering fix for global--advanced/--beginnerflags. - The real installed
pyfixconsole command (not just module invocation) was exercised forrun,doctor,analyze,undo, andexplain.
NOT EXECUTED:
- The real
pytestsuite (blocked by no network access in this sandbox — the test files need no changes to run under it elsewhere). - Any test requiring an actual
pip installagainst PyPI. - Windows-specific launcher paths (
.venv\Scripts\python.exe,py.exe) — handled as data viapathlib, but not exercised through a real Windows interpreter in this Linux sandbox.
KNOWN LIMITATIONS (Level 2):
- The static index-bounds check only tracks a variable assigned a
literal list/tuple exactly once in the file; if the list is
later mutated (
.append(...)) or reassigned, PyFix's tracked length can go stale and it will decline to flag that variable further (it errs toward silence, not a false positive, in ambiguous cases — but this means some genuinely-out-of-bounds accesses after mutation won't be caught). - Attribute-typo suggestions are always explanation-only, even at HIGH confidence — PyFix does not attempt to locate and edit the call site automatically for this category, unlike variable/keyword typos.
- The static wrong-argument-count check only understands locally-defined functions in the same file (no cross-file / cross- module call graph).
break/continue-outside-loop and most control-flow checks only run via the staticanalyze/doctorpath; Python's own compiler already raises aSyntaxErrorfor a whole file containing invalid control flow before PyFix would ever get a clean AST to analyze at runtime.pyfix logs,pyfix clear-logs, andpyfix diffremain stubs from V1 — they say so rather than faking output.- This build was developed and tested on Linux; Windows is the product's first-class target per spec but wasn't exercised on an actual Windows machine.
Full status of the original V1 limitations
All limitations documented in the original V1 README still apply except where superseded above (undefined-variable typos and unexpected- keyword typos are no longer "explanation-only" — they're now Level-2's flagship safe auto-fixes).
3.0.0 — Structural Syntax & Formatting Intelligence, and the "keep going" fix
This is a scoped, real (not aspirational) increment toward the 3.0 vision, not a full rewrite of every subsystem described in the design notes. What actually shipped in this pass:
- New subsystem:
pyfix.analysis.structural. Uses Python's own PEG-parser diagnostics (SyntaxError.msg/.lineno/.offset— themselves derived from real grammar analysis, not regex keyword matching) to classify a specific, well-understood set of structural problems:- missing block colon (
if/elif/else/for/while/def/class/try/except/finally/with/match/case) — HIGH confidence, auto-fixable - mismatched closing bracket (e.g.
(1, 2, 3]) — HIGH confidence, auto-fixable for a single, unambiguous mismatch - unclosed bracket — explanation-only; PyFix does not guess where the closing bracket belongs
- missing block body (
if x:with nothing indented under it) — explanation-only; PyFix does not invent the missing statement - unexpected/stray indentation — auto-fixable only in the unambiguous case (previous line doesn't open a block); otherwise explanation-only
- Correctly ignores multi-line expressions/collections, multi-line strings, and comments containing block keywords — it's driven by the parser's own error location, not string scanning.
- missing block colon (
StructuralSyntaxDetectorwires this into the existing detect→explain→propose→ask→apply→verify pipeline (pyfix.core.orchestrator), ahead of the old explanation-onlySyntaxErrorDetector/IndentationErrorDetector, which remain as the honest fallback for everything this subsystem can't yet classify.run_static_analysis(the engine behindpyfix analyze/pyfix doctor) no longer returns nothing the instant a file fails to parse — it now runs the structural analyzer so a whole-file scan still reports the most common beginner bug class instead of going silent.- The core architectural bug is fixed:
pyfix runused toreturnimmediately — ending the entire session — the moment it hit one error it couldn't recognize or couldn't safely auto-fix. It now explains that issue as before, then runs a whole-file static scan and reports anything else it can see, clearly labeled as execution-independent static findings. One unsolvable error no longer hides every other detectable one. - Public Python API (
import pyfix):pyfix.analyze(path),pyfix.diagnose(path),pyfix.apply_repair(path, proposal)— thin, documented wrappers around the existing internals, not a parallel implementation.repair()/verify()one-shot functions were deliberately not added, because PyFix's permission model requires an explicit approval step that a bare library call has no user to give; callers get aRepairProposaland decide. - Version bumped to 3.0.0 across
pyproject.toml,pyfix --version, andpyfix.__version__. - 39 new tests (
tests/structural/,tests/cli/,tests/test_version.py), all passing alongside the full pre-existing suite — see the development notes below for how this was verified in a network-disabled sandbox.
What was NOT done in this 3.0.0 pass
The three 3.0 design documents describe a much larger system —
root-cause dependency graphs across multiple errors, project-wide
multi-file analysis, AI-assisted whole-file reconstruction, a repair-
loop/fingerprinting engine, test-aware repair, library/API signature
introspection, and more. None of that was built in this pass. What's
here is a real, tested, working slice: the specific "stop on one
unsolvable issue" architectural bug called out as the top priority in
the design notes is fixed, and one full new detection subsystem
(structural syntax) was added end-to-end, rather than stubbing a
larger surface area with fake or partial implementations.
pyfix logs, pyfix clear-logs, pyfix diff, and JSON output landed
in 3.1.0 (below) — see that section for the corresponding audit.
3.1.0 — completing the advertised CLI surface, and an honest audit
3.0.0 shipped one real subsystem end-to-end. This pass is a full
repository audit: everything the CLI, README, and public API
advertised was checked against what actually ran, and every gap
found was either implemented for real or the advertising was
corrected — nothing was left as a pass/NotImplementedError stub or
silently removed.
Audit findings
The only genuine stubs in the whole codebase were three CLI
subcommands (grep for TODO/FIXME/NotImplementedError/stub
across src/ turned up nothing else):
if args.command in ("logs", "clear-logs", "diff"):
print("This feature is not implemented yet.")
return 0
Beyond that, one real CLI ergonomics bug was found and is a common
source of user confusion: --beginner/--advanced only worked
before the subcommand (pyfix --advanced run f.py); the same flag
after the subcommand or the script path (pyfix run f.py --advanced,
which is by far the more natural place to type it) raised
unrecognized arguments. analyze additionally had its own
independent --advanced flag definition that (silently, due to how
argparse subparser defaults interact with a top-level default) could
overwrite a --advanced passed before the subcommand back to False.
Everything else audited — the repair loop, confidence/risk levels,
backups/undo, permission prompts, Git-safety warnings, environment/
dependency checks, doctor/analyze/explain, and the existing 119
tests — was already real and working, and none of it was rewritten.
What was implemented
pyfix logs— a real, persistent, per-project diagnostic log (pyfix/state/diagnostic_log.py), JSON-Lines–backed so one corrupted line can never take down the rest of the log or crash the command. One entry is recorded per detected issue (not per CLI invocation) bypyfix run,pyfix explain, andpyfix undo, carrying category, severity, confidence, what happened, the proposed repair, whether it was applied/verified/rolled back, the repair-loop iteration number, and the PyFix version — never environment variables, secrets, or raw process state.--advancedshows full per-entry detail; the default view is one line per entry.pyfix logs --limit Ncaps how many are shown.pyfix clear-logs— deletes only the log file above. It always reports the exact path and entry count before doing anything, requires an interactive[Y/N]confirmation unless--yesis passed, and is a safe no-op (not an error) when there's nothing to clear or when called repeatedly. It cannot touch source files,.pyfix/backups, the undo log, or.git— it only ever unlinks the onelogs.jsonlpath it owns.pyfix diff— persists the most recent proposed or applied diff (pyfix/state/diff_store.py, one small JSON file, overwritten on each new proposal — not a growing history) so it can be retrieved from a separate, laterpyfix diffinvocation. Reports clearly whether the diff was applied or only proposed, handles "nothing yet" and a corrupted state file identically (an honest "no diff available," never a crash or a fabricated diff), and supports multi-file proposals.- The CLI flag-ordering bug is fixed.
--beginner/--advancednow work before the subcommand, after it, or after the target file, for every subcommand that renders a proposal or diagnostic (run,analyze,doctor,explain,logs). The two flags are mutually exclusive even when split across those positions (e.g.pyfix --beginner run f.py --advancedis rejected).analyze's duplicate, independent--advanceddefinition was removed in favor of the shared mechanism. pyfix analyze --json— structured, schema-stable JSON (file, issue count, and one object per issue with category/severity/ confidence/location/evidence/proposed-fix-summary). Plainjson.dumpsoutput only — no ANSI color, no Unicode icons — so it's safe for editors, CI, or another program to parse.analyzeitself was already, and remains, purely static: it parses and inspects the AST and never executes the target file's top-level code (there's now a regression test asserting this explicitly).- 39 new tests covering all of the above (log corruption/clearing/ safety boundaries, diff persistence/corruption, flag ordering in every position, JSON schema and the "never executes" guarantee), plus a permanent regression fixture for the exact "missing colon → typo → out-of-range index" workflow used throughout the design notes.
- Version bumped to 3.1.0 — a real, versionable feature addition on top of 3.0.0, not a patch release.
What was intentionally NOT built in this pass
Per-project state (logs, the last diff) stays inside the existing
<project_root>/.pyfix/ directory that backups and the undo log
already use, rather than introducing a second, global, platform-
specific state directory (e.g. via platformdirs) — that would be a
bigger architectural change for a benefit (state surviving a project
being moved/deleted) this pass didn't find a concrete need for, and it
would be inconsistent with how backups/undo already work. This can be
revisited if project-wide history across machines is ever needed.
Everything listed as NOT done at the end of the 3.0.0 section above is
still not done: project-wide/cross-file analysis, a call graph,
data-flow/root-cause tracing, test-aware repair, repair-loop
oscillation fingerprinting, and an AI-reasoner interface. These are
substantial, multi-file subsystems in their own right; claiming a
partial or fake version of any of them (a for file in project: analyze(file) loop labeled a "project index," for instance) would be
exactly the kind of overclaiming this project's own design principles
rule out. pyfix run/pyfix explain also do not yet have a --json
mode (only analyze does) — the interactive [Y/N] permission flow
that run/explain render doesn't yet have a defined
machine-readable equivalent, and inventing one without a concrete
consumer in mind risked guessing at a schema.
Windows compatibility was reasoned about (this module uses
pathlib.Path throughout, subprocess calls already use shell=False
and argv lists, and nothing added in this pass hardcodes a POSIX path
separator or a Unix-only assumption), but none of it was verified by
actually running on Windows — this sandbox is Linux-only, and claiming
"tested on Windows" without a Windows machine to test on would itself
be exactly the kind of false certainty this project exists to avoid.
Release files for PyFix-debugger 3.1.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 | |
|---|---|---|---|
| pyfix_debugger-3.1.0.tar.gz | 78.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyfix_debugger-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 164.4 kB
Release files / pyfix_debugger-3.1.0.tar.gz
| Download URL | pyfix_debugger-3.1.0.tar.gz |
|---|---|
| Size | 78.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f1dcbb0434029c668ec4aa31abf70f61db86d80106bf66b4d779bd0d3963ac48
|
|
BLAKE2b-256 checksum How to use checksums |
500c5db7603de2e1fd6a330bf5d6200f412b9aee2e12b76c66489ce93911a6f3
|
| 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 26, 2026.
Transparency logRelease files / pyfix_debugger-3.1.0-py3-none-any.whl
| Download URL | pyfix_debugger-3.1.0-py3-none-any.whl |
|---|---|
| Size | 86.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e1249dfc35115a6b7168c130047903cbd848dc7a76631ad99da3f83664ae8cee
|
|
BLAKE2b-256 checksum How to use checksums |
7848ceb2e5bb7b0cd035ce54f91f98a2981482298d9dbff4c8869305f67d7a98
|
| 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 26, 2026.
Transparency log