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.1.tar.gz (82.5 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.1-py3-none-any.whl (57.2 kB view details)

Uploaded Python 3

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

Hashes for clean_code-0.2.1.tar.gz
Algorithm Hash digest
SHA256 f7197ff5cd656b4481fb66ffbfcf3507a32d3b385bd8c4c34ae7e0edb70004fc
MD5 d112306d82e516c10c4e9967ccbac11d
BLAKE2b-256 bf963f7bfd303a294cd7710d40fb0bcfe18d651920c65b03aa1783af56c5b039

See more details on using hashes here.

Provenance

The following attestation bundles were made for clean_code-0.2.1.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.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

Hashes for clean_code-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 46b46fd81c31877e6081cb68db688683e86c6503968d7f054fbbf5c390593bd2
MD5 8e7755bd3cae3a14606affe648a12dcf
BLAKE2b-256 9c975a77a18e3f49f8ea8d6c51d6094959da636ee0f483bfd2f6a54fef35c6a7

See more details on using hashes here.

Provenance

The following attestation bundles were made for clean_code-0.2.1-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