Skip to main content

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: a messy LLM-generated function on the left becomes a short, well-named function on the right

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.

CI PyPI License: Apache 2.0 Python 3.11+ Only dependency: click No LLM calls, no API key


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 ast parsing, 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

Apache 2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

clean_code-0.2.0.tar.gz (82.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

clean_code-0.2.0-py3-none-any.whl (57.2 kB view details)

Uploaded Python 3

File details

Details for the file clean_code-0.2.0.tar.gz.

File metadata

  • Download URL: clean_code-0.2.0.tar.gz
  • Upload date:
  • Size: 82.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for clean_code-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a57ad8540804763f1f4941de310774f0cdfefa5cfa899ed776416414925e6428
MD5 f2528d7c552bc53012935797e0e3e9bc
BLAKE2b-256 d4f862d3418c93a8c2d79caf585e8bb0dc2a0a6989c61f2d7a06bdd069c93690

See more details on using hashes here.

Provenance

The following attestation bundles were made for clean_code-0.2.0.tar.gz:

Publisher: release.yml on LukasB24/clean-code

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file clean_code-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: clean_code-0.2.0-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

Hashes for clean_code-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 07f853e41d6565c5702c397cb2d2fd271c50fdb126e34b06c60d2964509acab4
MD5 8477c7277b078c9ede2a09b522d1b95a
BLAKE2b-256 2bd925be7d6146fb574d1820b40072f2d6f52ff76176bab31c16c5ff7300392d

See more details on using hashes here.

Provenance

The following attestation bundles were made for clean_code-0.2.0-py3-none-any.whl:

Publisher: release.yml on LukasB24/clean-code

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page