Deterministic ast-based linter for LLM-generated Python that catches readability and SOLID/DRY smells, pairing every violation with a concrete fix instead of a vague warning
Project description
clean-code
Your agent solved the hard problem. The code never explains how.
clean-code does that part — deterministically, so the next person to read it can tell.
Agents get the hard logic right. They don't make it easy to follow — deep nesting, terse names, comments that just restate the code. It works - nobody understands why.
clean-code closes that gap. Two things make it different from asking an LLM to review its own code:
- Finding violations is free and deterministic. It's plain
astparsing, not another model — same input, same output, every time, and every result traces back to the exact rule that raised it. - Every violation comes with an actual fix. Instead of a vague "too complex," you get a specific instruction — rename this, extract that, flatten this branch — that you, or the agent that wrote the code, can act on right away.
Install
pip install clean-code
That's it — you get a clean-code command. Requires Python 3.11+, and the
only dependency is click.
Working on clean-code itself? See CONTRIBUTING.md for
an editable install (pip install -e ".[dev]").
Quickstart
Point it at your code:
clean-code check src
Using it with Claude Code
If you want clean-code to run itself as part of a Claude Code session, copy the bundled skill into your user skills folder once:
mkdir -p ~/.claude/skills/ && cp -r .claude/skills/clean-code ~/.claude/skills/
Then trigger it directly within your Claude Code CLI or VS Code extension:
/clean-code [instruction or target_path]
See it catch something real
Given checkout.py, fresh out of an LLM:
def calc(u, o):
if u.active:
if o.total > 500:
discount = o.total * 0.15
else:
discount = o.total * 0.05
try:
charge(o, o.total - discount)
except:
pass
return discount
$ clean-code check checkout.py
checkout.py:
1:9: warning NM201 short parameter `u` (1 characters)
fix: use a descriptive name that states what the value represents
1:12: warning NM201 short parameter `o` (1 characters)
fix: use a descriptive name that states what the value represents
3:21: warning SM607 magic number `500` — extract it to a named, typed constant
fix: e.g. `SOME_DESCRIPTIVE_NAME = 500`
4:33: warning SM607 magic number `0.15` — extract it to a named, typed constant
fix: e.g. `SOME_DESCRIPTIVE_NAME = 0.15`
6:33: warning SM607 magic number `0.05` — extract it to a named, typed constant
fix: e.g. `SOME_DESCRIPTIVE_NAME = 0.05`
9:4: warning PY901 bare `except:` catches everything, including KeyboardInterrupt and SystemExit
fix: name the expected exception(s), e.g. `except (ValueError, KeyError):`
9:4: warning PY902 exception silently discarded — handler body does nothing to acknowledge the failure
fix: log it, return an explicit fallback, or re-raise — anything that leaves a trace
7 violation(s) in 1 file(s): 0 error(s), 7 warning(s), 0 info(s)
Nothing exotic — a couple of one-letter parameters, some magic numbers, and a swallowed exception. Exactly the kind of thing that slips through a quick glance at an LLM's output, and exactly what clean-code is for.
Violations are grouped by file (the path is printed once, not repeated per
line) and each carries a concrete fix suggestion — both cut tokens when the
tool is driven by an LLM. The rule_name is still available via --json or
cleancode rules, just not repeated inline since the message already says
what the rule name would.
By default info-severity violations (the fuzziest, lowest-signal rules) are
hidden — pass --min-severity info to see everything. --min-severity never
hides a violation that --fail-on/fail_on would fail the run on: setting
fail_on without min_severity lowers the display floor to match, so you
always see what can fail your build. Set min_severity explicitly to
override that.
Configure it for your project
Out of the box the defaults are strict (max nesting depth 2, max 3
function params, etc.) — good for shaking out problems, but you'll likely
want to loosen a few knobs for your codebase. Drop a [tool.cleancode] table
in your pyproject.toml:
[tool.cleancode]
disable = ["NM203"] # rules you don't want at all
fail_on = "warning" # info | warning | error — what fails the build
exclude = ["migrations/**"] # globs to skip entirely
[tool.cleancode.ST101]
max_depth = 3 # loosen the default nesting limit
[tool.cleancode.NM201]
allowed = ["i", "j", "k", "n", "x", "y", "_", "id", "ok", "fh", "df"] # your short names are fine
clean-code finds this automatically by walking up from whatever path you
check (or point it elsewhere with --config).
One line too noisy to fix right now? Suppress it inline instead of touching the config:
legacy_tmp = migrate(rows) # cleancode: disable=NM202
The rules
35 rules across 9 categories, each with a default severity you can override:
| Category | IDs | Count | Catches |
|---|---|---|---|
| Structure | ST101–ST107 | 7 | nesting, length, params, complexity, mixed responsibilities |
| Naming | NM201–NM203 | 3 | single-letter names, data/tmp/process_data, cryptic abbreviations |
| Comments & docstrings | CM301–CM304 | 4 | docstrings/comments that just restate the code |
| Subscripts | SL401–SL402 | 2 | x[i][j][k]-style complexity and chaining |
| Types | TY501 | 1 | uninformative Any |
| Structural smells | SM601–SM613 | 13 | magic numbers, nested comprehensions, redundant ternaries, PyTorch pitfalls, unused bindings, builtin shadowing, and more |
| SOLID | SD801–SD802 | 2 | type-switches violating OCP, low-cohesion classes |
| Duplication | DP701 | 1 | copy-pasted function bodies |
| Correctness | PY901–PY902 | 2 | bare except:, silently-discarded exceptions |
cleancode rules prints the full list from the CLI. For every rule's exact
default options, severity, and the edge cases each one accounts for (what's
exempt and why), see docs/RULES.md.
Disclaimer
The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, completeness, or correctness. In no event shall the authors be liable for any claim, damages, or other liability arising from the use of this software.
Contributing
Bug reports, new rule proposals, and PRs are welcome — see
CONTRIBUTING.md for the dev setup and a walkthrough of
adding a rule, CHANGELOG.md for what's shipped, and
RELEASING.md for how versions get published.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clean_code-0.2.1.tar.gz.
File metadata
- Download URL: clean_code-0.2.1.tar.gz
- Upload date:
- Size: 82.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7197ff5cd656b4481fb66ffbfcf3507a32d3b385bd8c4c34ae7e0edb70004fc
|
|
| MD5 |
d112306d82e516c10c4e9967ccbac11d
|
|
| BLAKE2b-256 |
bf963f7bfd303a294cd7710d40fb0bcfe18d651920c65b03aa1783af56c5b039
|
Provenance
The following attestation bundles were made for clean_code-0.2.1.tar.gz:
Publisher:
release.yml on LukasB24/clean-code
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clean_code-0.2.1.tar.gz -
Subject digest:
f7197ff5cd656b4481fb66ffbfcf3507a32d3b385bd8c4c34ae7e0edb70004fc - Sigstore transparency entry: 2171728780
- Sigstore integration time:
-
Permalink:
LukasB24/clean-code@d2accd497bfa539a16ade86d4530ac27297f2bd8 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LukasB24
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d2accd497bfa539a16ade86d4530ac27297f2bd8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file clean_code-0.2.1-py3-none-any.whl.
File metadata
- Download URL: clean_code-0.2.1-py3-none-any.whl
- Upload date:
- Size: 57.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46b46fd81c31877e6081cb68db688683e86c6503968d7f054fbbf5c390593bd2
|
|
| MD5 |
8e7755bd3cae3a14606affe648a12dcf
|
|
| BLAKE2b-256 |
9c975a77a18e3f49f8ea8d6c51d6094959da636ee0f483bfd2f6a54fef35c6a7
|
Provenance
The following attestation bundles were made for clean_code-0.2.1-py3-none-any.whl:
Publisher:
release.yml on LukasB24/clean-code
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clean_code-0.2.1-py3-none-any.whl -
Subject digest:
46b46fd81c31877e6081cb68db688683e86c6503968d7f054fbbf5c390593bd2 - Sigstore transparency entry: 2171728784
- Sigstore integration time:
-
Permalink:
LukasB24/clean-code@d2accd497bfa539a16ade86d4530ac27297f2bd8 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LukasB24
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d2accd497bfa539a16ade86d4530ac27297f2bd8 -
Trigger Event:
release
-
Statement type: