Trust, but verify. The quality gate for AI-generated Python code.
AI coding agents ship code fast — but fast doesn't mean safe. Every eval() an LLM drops in, every mutable default it forgets, every circular import it creates is a landmine waiting to go off. PyCodeGate is the trust layer between AI-generated code and your production codebase: one command, one score, zero ambiguity.
PyCodeGate detects your project's context: framework (Django, FastAPI, Flask), Python version, package manager (uv, poetry, pip), and test framework. That context drives which rules are active — Django projects get SQL injection checks, FastAPI projects get async-correctness checks, and so on.
It then runs analysis passes in parallel: a lint pass across 9 categories (Security, Correctness, Complexity, Architecture, Performance, Structure, Imports, Dependencies, Dead Code), a dead-code pass powered by Vulture that finds unused functions, classes, imports, and variables, and an optional dependency-vulnerability pass via pip-audit.
PyCodeGate is precision-first: it is meant to stay quiet on idiomatic, correct code so that a finding always means something. Test files only receive rules that make sense for tests (asserts, long fixtures, and long test functions are not flagged), and generated or non-source trees — migrations, examples, docs, and vendored directories — are skipped. The rule set is benchmarked against a corpus of respected PyPI packages (see benchmarks/) to keep false positives down.
Findings are filtered through your configuration and scored using a weighted category-budget system. Each category has a maximum deduction budget proportional to its weight. Within a category the top 3 findings apply at full cost; additional findings apply diminishing returns (10% each), so fixing the worst issues always moves the needle. The final result is a 0–100 health score with a label: Excellent (90+), Great (75–89), Needs work (50–74), or Critical (<50).
Run instantly with uvx — no install needed:
uvx pycodegate .Install globally with pipx or uv:
pipx install pycodegate
# or
uv tool install pycodegate
# or
pip install pycodegate# Basic scan — score + summary
pycodegate .
# Show file paths and line numbers for every finding
pycodegate . --verbose
# Machine-readable output for AI agents and CI
pycodegate . --json
# Auto-fix ruff-fixable issues, then scan
pycodegate . --fix
# Output only the numeric score (useful in scripts)
pycodegate . --score
# Scan only files changed vs a base branch
pycodegate . --diff mainPass --json to get structured output that AI agents and CI pipelines can parse:
pycodegate . --json{
"version": "0.3.0",
"path": ".",
"score": 87,
"label": "Great",
"errors": 1,
"warnings": 4,
"elapsed_ms": 212,
"project": {
"framework": "fastapi",
"python_version": "3.12",
"package_manager": "uv",
"test_framework": "pytest"
},
"diagnostics": [
{
"rule": "no-mutable-default",
"severity": "error",
"category": "Correctness",
"message": "Mutable default argument `[]` is shared across all calls",
"file_path": "src/api/routes.py",
"line": 34
},
{
"rule": "high-complexity",
"severity": "warning",
"category": "Complexity",
"message": "Function 'process_order' has cyclomatic complexity 17 (max 15)",
"file_path": "src/api/orders.py",
"line": 88
}
]
}PyCodeGate is designed to be used by AI coding agents. Add it to your agent's context so it runs after every Python change.
Add the skill to your project:
mkdir -p .claude/skills
curl -fsSL https://raw.githubusercontent.com/themohitkhare/pycodegate/main/skills/pycodegate/SKILL.md \
-o .claude/skills/pycodegate.mdOr copy AGENTS.md to your project root — Claude Code picks it up automatically.
Add to .cursor/rules/pycodegate.mdc:
After modifying Python files, run `uvx pycodegate . --json` and fix all findings
with severity "error" before marking the task complete. Target score: 80+.Add to .windsurfrules:
After modifying Python files, run: uvx pycodegate . --json
Fix all "error" severity findings. Re-run to verify the score improved.
Add to your system prompt:
After modifying Python files, run `uvx pycodegate . --json` to check code quality.
Fix errors first. Target score: 80+.
aider --read AGENTS.mdname: Quality Gate
on: [push, pull_request]
jobs:
pycodegate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for --diff
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run PyCodeGate
run: |
pip install pycodegate
pycodegate . --verbose --diff main --fail-on error| Flag | Default | Description |
|---|---|---|
[DIRECTORY] |
. |
Path to the Python project to scan |
--lint / --no-lint |
on | Enable or disable lint checks |
--dead-code / --no-dead-code |
on | Enable or disable dead code detection |
--verbose |
off | Show file path and line number per finding |
--score |
off | Print only the numeric score and exit |
--json |
off | Emit structured JSON (for agents and CI) |
--sarif |
off | Emit SARIF 2.1.0 (for GitHub Code Scanning) |
--fix |
off | Run ruff --fix before scanning |
--diff TEXT |
— | Scan only files changed vs this base branch |
--fail-on [error|warning|none] |
none |
Exit code 1 when findings at this level exist |
--min-score INT |
— | Exit code 1 when the score is below this threshold |
--profile [cli|web|library|script] |
auto | Override the auto-detected project profile |
--badge |
off | Print a shields.io badge markdown snippet |
--ci |
off | Print a ready-to-use GitHub Actions workflow |
--pre-commit |
off | Install a git pre-commit hook |
-v, --version |
— | Show version and exit |
-h, --help |
— | Show help and exit |
| Category | Weight | What it catches |
|---|---|---|
| Security | 5 | eval, exec, pickle.load, unsafe YAML, hardcoded secrets, weak hashes (excluding usedforsecurity=False), os.system, shell=True, tempfile.mktemp |
| Correctness | 4 | Mutable defaults, bare except, assert in production, bad __init__ return |
| Complexity | 3 | Cyclomatic complexity > 15 (warning) or > 25 (error) |
| Architecture | 3 | Giant modules (>500 lines), deep nesting (>5), god functions (>50 lines), too many args (>7) |
| Performance | 2 | String concatenation in loops, star imports |
| Structure | 2 | Missing tests, README, LICENSE, linter/type-checker config, low type coverage |
| Imports | 1 | Circular imports between sibling modules |
| Dependencies | 1 | Known-vulnerable dependencies (via pip-audit, when installed) |
| Dead Code | 1 | Unused functions, classes, variables, and imports via Vulture |
Weights are normalized to a 100-point deduction budget. Framework- and library-specific rules (Django, FastAPI, Flask, Pydantic, SQLAlchemy, Celery, requests, logging, pandas, pytest, NumPy) are mapped into the Security or Correctness budget.
PyCodeGate uses a weighted category-budget system:
- Each category has a weight (see table above). Weights are normalized to sum to 100 points of total deduction budget.
- Within a category, findings are sorted by cost (errors cost more than warnings). The top 3 findings apply at full cost; every additional finding applies diminishing returns (10% of its cost).
- A category's deduction is capped at its budget, so a single broken category can never zero out an otherwise healthy project.
- Final score =
100 - sum(capped category deductions), floored at 0.
| Score | Label | Meaning |
|---|---|---|
| 90–100 | Excellent | Production-ready |
| 75–89 | Great | Minor issues to address |
| 50–74 | Needs work | Significant issues present |
| 0–49 | Critical | Blocking issues, do not ship |
Create pycodegate.toml in your project root:
[options]
lint = true
dead_code = true
verbose = false
fail_on = "none"
[ignore]
rules = ["dead-code", "no-import-in-function"]
files = ["tests/fixtures/**", "migrations/**", "scripts/**"]
[per-file-ignores]
"src/legacy/*.py" = ["high-complexity", "no-god-function"]
[scoring]
max-deduction = { Security = 20, "Dead Code" = 0 } # 0 disables a category's penaltyOr use pyproject.toml:
[tool.pycodegate]
lint = true
dead_code = true
fail_on = "error"
[tool.pycodegate.ignore]
rules = ["no-import-in-function"]
files = ["tests/fixtures/**"]
[tool.pycodegate.per-file-ignores]
"src/legacy/*.py" = ["high-complexity"]
[tool.pycodegate.scoring]
"max-deduction" = { Security = 20, "Dead Code" = 0 }If both files exist, pycodegate.toml takes precedence. CLI flags always override config values.
PyCodeGate auto-detects a project profile and adjusts rule weights accordingly. You can also set a profile explicitly in config (profile = "library").
| Profile | Auto-detected when | Adjustments |
|---|---|---|
cli |
[project.scripts] or a CLI dependency (click, typer, …) |
Security budget relaxed; subprocess/shell rules suppressed |
web |
Django / FastAPI / Flask / Starlette / aiohttp / … detected | Framework rules active |
library |
Has a [build-system] and no scripts |
Default budgets |
script |
No package __init__.py and ≤ 5 top-level .py files |
Structure budget relaxed; no-tests/no-license suppressed |
--pre-commit installs a git pre-commit hook (one-time) that scores the project on
every commit and blocks the commit when the score falls below the threshold:
pycodegate . --pre-commit --min-score 80If you use the pre-commit framework, add it to
.pre-commit-config.yaml instead:
repos:
- repo: local
hooks:
- id: pycodegate
name: PyCodeGate quality check
entry: pycodegate . --fail-on error
language: system
types: [python]
pass_filenames: falsegit clone https://github.com/themohitkhare/pycodegate
cd pycodegate
uv sync --all-extras
uv run pytest -q
uv run pycodegate . --verbose # dogfood itTo add a new rule:
- Create a file in
src/pycodegate/rules/extendingBaseRules - Implement
check(self, source: str, filename: str) -> list[Diagnostic] - Register it in
src/pycodegate/rules/__init__.py - Add tests in
tests/rules/
MIT