PyFixer
The auto-fix button for Python — conservative mechanical fixes by default, LLMs only behind behavioral gates.
Upload a broken file. Get a fixed file. That's it.
Upload → Scan → Fix → Verify → Contracts → Done
What It Does
PyFixer finds and fixes Python problems automatically.
| Problem | Fix |
|---|---|
| Security holes | Replaces eval, exec, hardcoded passwords, weak crypto |
| Unused imports | Removes them |
| Wrong types | Adds type annotations |
| Bad style | Formats code, fixes line length |
| Missing docs | Adds docstrings |
| Old Python | Modernizes to current syntax |
| Bare excepts | Changes to specific exceptions |
| Print statements | Converts to logging |
| Mutable defaults | Fixes def foo(x=[]) |
| Builtin shadowing | Fixes list = x |
| Type comparison | Fixes type(x)==type(y) |
| Performance | Fixes O(n²) loops, string concat |
| Weak random | Flags non-crypto random in security contexts (no blind swap: secrets returns a different type) |
| Weak hash | Replaces MD5 with SHA256 |
| Logic bugs | Behavioral tests catch wrong operators, crash-on-empty, stubs |
17+ types of issues. One click to fix.
How It Works (v0.4.0)
1. Upload .py file
↓
2. Scan with ruff + bandit + mypy + custom/semantic checks
↓
3. Generate a behavioral test for EVERY function (coverage guarantee)
↓
4. AI proposes a fix (13 specialist workers)
- clean functions are never sent to the LLM
↓
5. Post-processing layer applies guaranteed fixes
- hardcoded secrets are scrubbed from the output
↓
6. CONTRACT GATE: probe original vs fixed behavior
- untouched functions must return identical results
- perf/logging-only fixes must be value-identical
- violations -> corrective retry -> verbatim splice
↓
7. Verify: syntax check + ruff + mypy + findings count
↓
8. You approve or reject
↓
9. Done. File is fixed.
Features
13 Specialist Workers
Each worker is an expert in one thing:
| Worker | What It Fixes |
|---|---|
| Syntax Surgeon | Syntax errors, indentation |
| Security Guard | SQL injection, hardcoded passwords, weak crypto |
| Type Tamer | Missing type annotations, wrong types |
| Style Formatter | Line length, imports, formatting |
| Code Cleaner | Dead code, unused variables |
| Docstring Writer | Missing docstrings, D101-D107 |
| Annotation Expert | Missing annotations, ANN001-ANN204 |
| Modernizer | Old Python syntax, PTH/SIM/RET |
| Performance Optimizer | O(n²) loops, string concatenation |
| Logger | print() → logging conversion |
| Error Handler | Bare excepts, broad exceptions |
| Generalist | Complex issues, multiple codes |
| MyPy Fixer | Type ignore comments, mypy-specific |
BYOK (Bring Your Own Key)
You bring your own API key. We never store it.
| Provider | Key prefix | Model |
|---|---|---|
| Google Gemini | AQ. |
gemini-3.7-flash |
| DeepSeek | sk- (probed) |
deepseek-chat |
| OpenRouter | sk-or- |
deepseek/deepseek-v4-flash |
| OpenAI | sk- |
gpt-4o-mini |
Server-side key pool (optional)
Tired of pasting a key into the UI? Configure keys on the server and every client can fix without one:
# Option A: environment variable (comma or newline separated)
export PYFIXER_KEYS="AQ.Ab8RN6...,AQ.Ab8RN7..."
# Option B: key file, one key per line
$EDITOR data/gemini.keys # gitignored — never commit this file
Keys rotate automatically (N keys = N × 15 requests/min). The pool is only
used when a request carries no X-Api-Key header / body key; client keys
always take priority and are never stored, logged, or written to disk.
GET /api/server-keys reports { "available": true } without exposing them.
LLM control
Every model call goes through a task profile (temperature / token budget /
JSON-mode) so behaviour is tunable in one place — PROFILES in
pyfixer/byok.py:
| Profile | Used for | Temperature | Max tokens |
|---|---|---|---|
fix |
code fixes | 0.0 | 4096 |
fix_escalate |
contract-violation retries | 0.0 | 4096 |
testgen |
behavioral test generation | 0.0 | 3072 |
classify |
finding classification | 0.0 | 512 + JSON mode |
probe |
cheap probes | 0.0 | 256 |
Model resolution order: per-call override → PYFIXER_MODEL env var →
provider default. The classifier requests provider-side JSON mode (Gemini
response_mime_type, OpenAI-compatible response_format).
Verify & Rollback
- Verify: Proves the fix works (syntax + ruff + mypy + findings)
- Rollback: Undo if you don't like it
Pattern Learning
PyFixer remembers what works. The more you use it, the better it gets.
Quickstart
The fastest way — install from PyPI (no repo clone needed):
# Recommended: isolated install via pipx
pipx install pyfixer-ai
pyfixer scan app.py
# Or with pipx unavailable, a virtualenv:
python3 -m venv ~/.venvs/fixer
~/.venvs/fixer/bin/pip install pyfixer-ai
~/.venvs/fixer/bin/pyfixer scan app.py
On Debian/Ubuntu, plain
pip install pyfixer-airefuses with "externally-managed-environment" — that's your system Python protecting itself. Use pipx or a venv as above, never--break-system-packages.
From source (for development):
# Clone
git clone <your-repo>
cd pyfixer
# Setup
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# Run
.venv/bin/uvicorn pyfixer.main:app --host 127.0.0.1 --port 8501
Note:
python-multipartis required (pulled in byrequirements.txt) so the upload endpoints that useForm(...)work.ruffandbanditmust be installed and onPATH(the scanner shells out topython -m ruff/python -m bandit).mypyis optional — only used when you enable the mypy scanner.
CLI
Fix files straight from the terminal — same pipeline, same gates, no server:
# module form (works from a checkout)
python -m pyfixer fix app.py
# after `pip install -e .` you also get the console script
pyfixer scan app.py # findings only, no AI call (exit 1 on bugs; style hidden unless --all)
pyfixer fix app.py # writes app.fixed.py next to it
pyfixer fix app.py --apply # overwrite in place
pyfixer fix app.py --diff # print the patch, write nothing
pyfixer fix src/ # every .py under a folder (summary at end)
pyfixer fix app.py --format json # machine-readable output
pyfixer fix app.py --worker security # force a specialist
pyfixer fix app.py --llm # + LLM fixes (needs API key)
pyfixer fix app.py --llm --fast # fewer model calls, same gates
pyfixer fix app.py --llm --verify tests/test_app.py --apply # real-test gate (recommended)
pyfixer fix app.py --llm --apply --unsafe # write without any gate (you accept regressions)
pyfixer fix app.py --aggressive # + mechanical ruff upgrades (no key)
pyfixer fix app.py --check # preview only, write nothing
pyfixer workers # list all specialists
# key: --key beats $PYFIXER_KEYS beats data/gemini.keys (rotating pool)
# LLM-gated flags: --comprehensive and --reference work ONLY with --llm;
# --verify needs --llm (pytest gate) or --llm-targeted (mypy gate).
# Without them the CLI exits 2 instead of silently ignoring the flag.
# --llm --apply refuses to write without --verify (real-test gate) unless
# --unsafe is passed explicitly.
Deterministic Fixes (no API key needed)
Deterministic mode (the default — no API key needed) applies 50+ pattern-based fixes instantly:
| Code | Fix |
|---|---|
| C408 | dict(name="test") → {'name': 'test'} |
| C416 | [x for x in y] → list(y) |
| E711 | x == None → x is None |
| E712 | x == True → x |
| E722 | except: → except Exception: |
| F401 | Remove unused imports |
| F841 | Remove unused variables |
| LOGIC005/009 | Fix inverted comparison signs |
| LOGIC010 | Fix insertion sort off-by-one |
| LOGIC014 | def f(x=[]) → def f(x=None) |
| LOGIC015 | type(x) == int → isinstance(x, int) |
| LOGIC016 | for i in range(n) → for _ in range(n) |
| PLR1714 | x==1 or x==2 → x in (1, 2) |
| RET505 | Remove unnecessary else after return |
| SIM102 | if x: if y: → if x and y: |
| SIM105 | try: pass → contextlib.suppress() |
| SIM115 | open() → with open() |
| SIM116 | if k in d: return d[k] → d.get(k) |
| SIM118 | x in d.keys() → x in d |
| SIM210 | True if x else False → bool(x) |
| SIM212 | if x: return True else: return False → return x |
| UP030 | "{}".format(x) → f"{x}" |
Example:
# Quick fix without API key (deterministic is the default)
pyfixer fix buggy.py
# See what would change
pyfixer fix buggy.py --diff
# Apply in place
pyfixer fix buggy.py --apply
Exit codes: 0 OK · 1 findings survived the fix · 2 usage/no key.
API
| Method | Endpoint | Description |
|---|---|---|
| GET | /healthz |
Health check |
| POST | /api/uploads |
Upload + auto-scan |
| GET | /api/uploads |
List all files |
| GET | /api/uploads/{id} |
Get file details |
| POST | /api/uploads/{id}/fix |
Propose AI fix |
| POST | /api/uploads/{id}/verify |
Verify fix works |
| POST | /api/uploads/{id}/rollback |
Undo fix |
| GET | /api/fixes/{id}/approve |
Approve fix |
| GET | /api/fixes/{id}/reject |
Reject fix |
| GET | /api/uploads/{id}/report |
Download report |
| GET | /api/workers |
List all workers |
| GET | /api/audit |
Audit trail |
Scanner
PyFixer uses multiple scanners:
- ruff — Fast Python linter (16 rule sets)
- bandit — Security scanner
- mypy — Type checker
- AST detector — 16 LOGIC codes + 10 STYLE codes + 5+ ALGO/PERF codes
- Custom checks — 8 additional checks:
- ERR001: Missing error handling
- ERR002: Broad exception catching
- LOG001: print() instead of logging
- PERF001: Inefficient string concatenation
- MUT001: Mutable default arguments
- SHADOW001: Builtin name shadowing
- TYPE001: type() comparison instead of isinstance()
Total: 50+ types of issues detected.
Testing
PYTHONPATH=. .venv/bin/pytest tests/ -v
862 tests passing (incl. 9 opt-in corpus parity suites).
Regression benchmarks (LLM-in-the-loop)
benchmarks/ contains bug files with human-verified correct.py references
and check.py auto-checkers. The harness runs each through the REAL fix
pipeline and scores it — including clean canary functions that must keep
identical behavior (contract-regression guard).
# all cases (uses server key pool or PYFIXER_BENCH_KEY)
.venv/bin/python benchmarks/run_bench.py
# one case
.venv/bin/python benchmarks/run_bench.py --only logic_signs
Cases: algorithms, strings, mixed, security_basics, crash_edges,
logic_signs, perf_logging. A manual GitHub Actions workflow
(.github/workflows/regression.yml) runs them on demand with a
PYFIXER_KEYS secret.
Corpus regression (downstream suite parity)
tests/test_corpus_regression.py runs nine real projects' test suites against
both the pristine checkout and the deterministic rewrite, asserting identical
pass/fail signatures — the harness that caught the RET505 click corruption,
the ERR003 logging-import breakage, and the F401 # noqa side-effect import
deletion. Opt-in via PYFIXER_CORPUS_DIR:
bash scripts/provision_corpus.sh /path/to/pyfixer-corpus # checkouts + venvs
PYFIXER_CORPUS_DIR=/path/to/pyfixer-corpus PYTHONPATH=. pytest tests/test_corpus_regression.py -m corpus -q
Repos: click, httpie, requests, rich, flask, fastapi, attrs (2022-era),
httpx 1.0b0 (2025-era), pandas 2.2.3. Pandas is special: its compiled _libs
can't be PYTHONPATH-shadowed, so it runs as a dedicated two-venv parity
test (venv-pandas vs venv-pandas-fixed, both on numpy==1.26.4). A manual +
weekly workflow (.github/workflows/corpus.yml) runs the same in CI.
tests/test_semantic_rules.py locks the semantic tier (SEM003/SEM004/SEM006)
with a detector mutation oracle plus differential-execution coverage of the
SEM004 fixer; tests/test_mock_llm_contract.py locks the SEM003
detector -> per-function-LLM wiring with a stubbed model (no network).
Code quality
# Lint + auto-format
.venv/bin/ruff check pyfixer
.venv/bin/ruff format pyfixer
# Static type checking
.venv/bin/mypy pyfixer
A GitHub Actions workflow (.github/workflows/ci.yml) runs ruff check,
ruff format --check, mypy, and the full pytest suite on every push and
pull request, so regressions are caught before merge.
Tech Stack
- Backend: FastAPI + SQLite + Python
- Scanner: ruff + bandit + mypy + custom AST checks
- AI: OpenAI, OpenRouter, Google, DeepSeek (BYOK)
- Testing: pytest + coverage
Tier Semantics
PyFixer promotes fixes through four tier levels. The goal is honest tier assignment — a function's tier reflects the evidence that validates its rewrite.
| Tier | When it's assigned | Promotion path |
|---|---|---|
UNVERIFIED |
Default for any fixed function that has no oracle (no func_test). LOGIC‑only fixes (ast‑detector findings) always land here, regardless of verify_mode. |
After the real project suite passes (--verify in cli.py), tiers are promoted by _promote_tiers_to_verified in cli.py:1168/1276. |
AI_VERIFIED |
A generated func_test exists and the rewrite passes _valid_fix. The rewrite was validated against the AI‑generated test. |
Same real‑suite promotion; this is the interim tier after propose. |
VERIFIED |
Earned only after the real project suite (--verify) runs successfully via _promote_tiers_to_verified. No automatic path mint VERIFIED at propose time. |
Manual / CI gate after suite runs. |
RETRIEVED |
Deterministic fixes that match an existing reference (e.g. RETRIEVED from known‑good repo). | Stays RETRIEVED; no further promotion. |
Honesty invariant: verify_mode=True does not mint VERIFIED. It only means the user passed --verify; a real suite still needs to run afterwards. Promotion to VERIFIED belongs to the real‑suite gate in cli.py, never propose().
License
Copyright (c) 2026 Aziz. All rights reserved.
Author
Aziz — Built PyFixer to fix Python code automatically.
Release files for pyfixer-ai 0.4.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyfixer_ai-0.4.3.tar.gz | 506.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyfixer_ai-0.4.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 893.9 kB
Release files / pyfixer_ai-0.4.3.tar.gz
| Download URL | pyfixer_ai-0.4.3.tar.gz |
|---|---|
| Size | 506.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dd6fec5b8091a806dc09596cbb2e561489b8ef7da6b6ea3301b3aa3a139ecf67
|
|
BLAKE2b-256 checksum How to use checksums |
3fe238f91f1e795d284438ca97db96cdd511ff6591bab2d58e89482623d86e02
|
| 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 19, 2026.
Transparency logRelease files / pyfixer_ai-0.4.3-py3-none-any.whl
| Download URL | pyfixer_ai-0.4.3-py3-none-any.whl |
|---|---|
| Size | 387.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
15980ed4b01984bcb87170875e9dd8984ec9a4a1def5d5175f0bdbf669f697de
|
|
BLAKE2b-256 checksum How to use checksums |
b8df36a9aaff19358fa50d6d51187dd211a4789f0953131fb984797f1c265d71
|
| 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 19, 2026.
Transparency log