python-refactoring-c29923 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited python-refactoring-c29923 (Plugin) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<p align="center"> <img src="https://raw.githubusercontent.com/cheickmec/smellcheck/main/assets/logo.png" alt="smellcheck logo" width="200"> </p>
<h1 align="center">smellcheck</h1>
<p align="center"> <strong>Python Code Smell Detector & Refactoring Guide</strong><br> 83 refactoring patterns · 60 automated AST checks · zero dependencies </p>
<p align="center"> <a href="https://pypi.org/project/smellcheck/"><img src="https://img.shields.io/pypi/v/smellcheck" alt="PyPI"></a> <a href="https://pypi.org/project/smellcheck/"><img src="https://img.shields.io/pypi/pyversions/smellcheck" alt="Python"></a> <a href="https://github.com/cheickmec/smellcheck/actions/workflows/ci.yml"><img src="https://github.com/cheickmec/smellcheck/actions/workflows/ci.yml/badge.svg" alt="CI"></a> <a href="https://codecov.io/gh/cheickmec/smellcheck"><img src="https://codecov.io/gh/cheickmec/smellcheck/branch/main/graph/badge.svg" alt="Coverage"></a> <a href="https://pypistats.org/packages/smellcheck"><img src="https://img.shields.io/pypi/dm/smellcheck" alt="Downloads"></a> <a href="https://github.com/cheickmec/smellcheck/blob/main/docs/installation.md#pre-commit"><img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit" alt="pre-commit"></a> <a href="https://github.com/cheickmec/smellcheck/blob/main/LICENSE"><img src="https://img.shields.io/github/license/cheickmec/smellcheck" alt="License"></a> </p>
smellcheck is a Python code smell detector and refactoring catalog. It works as a pip-installable CLI, GitHub Action, pre-commit hook, or Agent Skills plugin for AI coding assistants.
No dependencies. Pure Python stdlib (ast, pathlib, json). Runs anywhere Python 3.10+ runs.
What are code smells? Code smells are surface-level patterns in source code that hint at deeper design problems — not bugs, but structural weaknesses that make code harder to maintain, extend, or understand. Learn more →
pip install smellcheck
smellcheck src/
smellcheck myfile.py --format json
smellcheck src/ --min-severity warning --fail-on warningAlso available as a GitHub Action, pre-commit hook, SARIF/Code Scanning integration, [Agent Skills](https://agentskills.io) plugin, and Cursor native plugin for Claude Code, Cursor, Copilot, Gemini CLI, and more.
[Full installation guide →](https://github.com/cheickmec/smellcheck/blob/main/docs/installation.md)
# Scan a directory
smellcheck src/
# Scan multiple files
smellcheck file1.py file2.py
# JSON output
smellcheck src/ --format json
# GitHub Actions annotations
smellcheck src/ --format github
# SARIF output (for GitHub Code Scanning)
smellcheck src/ --format sarif > results.sarif
# JUnit XML output (for Jenkins, GitLab, CircleCI, Azure DevOps)
smellcheck src/ --format junit > smellcheck-results.xml
# GitLab CodeClimate output (for MR code quality widget)
smellcheck src/ --format gitlab > gl-code-quality-report.json
# Filter by severity
smellcheck src/ --min-severity warning
# Control exit code
smellcheck src/ --fail-on warning # exit 1 on warning or error
smellcheck src/ --fail-on info # exit 1 on any finding
# Run only specific checks
smellcheck src/ --select SC101,SC701,SC210
# Skip specific checks
smellcheck src/ --ignore SC601,SC202
# Module execution
python3 -m smellcheck src/
# Generate a baseline of current findings
smellcheck src/ --generate-baseline > .smellcheck-baseline.json
# Only report findings not in the baseline
smellcheck src/ --baseline .smellcheck-baseline.json
# Disable caching for a fresh scan
smellcheck src/ --no-cache
# Use a custom cache directory
smellcheck src/ --cache-dir .my-cache
# Clear cached results
smellcheck --clear-cache
# Show documentation for a rule (description + before/after example)
smellcheck --explain SC701
# List all rules in a family
smellcheck --explain SC4
# List all rules grouped by family
smellcheck --explain all
# Generate a phased refactoring plan
smellcheck src/ --plan
smellcheck src/ --plan --format jsonsmellcheck reads [tool.smellcheck] from the nearest pyproject.toml:
[tool.smellcheck]
extends = "base.toml" # inherit from a shared config file
select = ["SC101", "SC201", "SC701"] # only run these checks (default: all)
ignore = ["SC601", "SC202"] # skip these checks
per-file-ignores = {"tests/*" = ["SC201", "SC206"]} # per-path overrides
fail-on = "warning" # override default fail-on
format = "text" # override default format
baseline = ".smellcheck-baseline.json" # suppress known findings
cache = true # enable file-level caching (default: true)
cache-dir = ".smellcheck-cache" # cache directory (default: .smellcheck-cache)CLI flags override config values. Precedence: CLI flags > environment variables > pyproject.toml > defaults.
Set SMELLCHECK_* variables for CI/CD without per-repo config files:
SMELLCHECK_FAIL_ON=warning SMELLCHECK_FORMAT=json smellcheck src/Supported: SMELLCHECK_MIN_SEVERITY, SMELLCHECK_FAIL_ON, SMELLCHECK_FORMAT, SMELLCHECK_SELECT, SMELLCHECK_IGNORE, SMELLCHECK_BASELINE. See the Configuration Reference for details.
extends)Use extends to inherit settings from a shared base config:
# base.toml — shared across repos
[tool.smellcheck]
ignore = ["SC601"]
fail-on = "warning"# pyproject.toml — project overrides
[tool.smellcheck]
extends = "base.toml"
ignore = ["SC202"] # adds to base; final ignore = ["SC601", "SC202"]Multiple bases are supported — later entries override earlier ones for scalar values, while ignore lists are unioned and per-file-ignores are deep-merged:
extends = ["base.toml", "strict.toml"]Paths are relative to the file containing the extends key. Chains are resolved recursively (up to 5 levels deep).
See the Configuration Reference for the full CLI flag table, JSON/baseline schemas, exit codes, and flag interaction details.
Add # noqa: SC701 to a line to suppress that check on that line:
def foo(x=[]): # noqa: SC701
return xUse # noqa (no codes) to suppress all findings on that line. Multiple codes: # noqa: SC601,SC202
Disable specific checks for a range of lines with # smellcheck: disable / # smellcheck: enable:
# smellcheck: disable SC301, SC305
class LegacyGodObject:
"""This class is intentionally large for backward compatibility."""
def method_one(self):
self._temp = compute() # SC305 suppressed by block directive
def method_two(self):
use(self._temp)
# smellcheck: enable SC301, SC305Disable all checks for a range:
# smellcheck: disable-all
# ... everything in this range is suppressed ...
# smellcheck: enable-allSuppress checks for an entire file (place at top of file):
# smellcheck: disable-file SC301, SC305Use # smellcheck: disable-file (no codes) to suppress all checks for the entire file.
disable / enable apply from that line to the matching enable (or end of file if no match)disable-all / enable-all work the same way but for all checks at oncedisable-file applies to the entire file# noqa still works alongside block directivesper-file-ignores in config instead)For large codebases, you can adopt smellcheck incrementally using a baseline file. The baseline records fingerprints of existing findings so only new issues are reported.
# 1. Generate a baseline from the current state
smellcheck src/ --generate-baseline > .smellcheck-baseline.json
# 2. Run with the baseline — only new findings are reported
smellcheck src/ --baseline .smellcheck-baseline.json
# 3. Or set it in pyproject.toml so every run uses it automaticallyFingerprints are resilient to line-number changes — renaming or moving code around won't break the baseline. When you fix a baselined smell, its entry is silently ignored.
--generate-baseline and --baseline are mutually exclusive.
Focus on files you actually changed — skip the rest of the codebase:
# Only scan files changed vs. main branch
smellcheck src/ --diff main
# Only scan files changed in the last commit
smellcheck src/ --diff HEAD~1
# Only scan uncommitted changes (shorthand for --diff HEAD)
smellcheck src/ --changed-onlyIn CI, this keeps PR feedback fast and relevant:
- uses: cheickmec/smellcheck@v0
with:
diff: origin/main
fail-on: warningCross-file checks (cyclic imports, shotgun surgery, etc.) run on the changed file set only. This is best-effort — for full cross-file accuracy, run without --diff.
--diff and --generate-baseline are mutually exclusive. --diff composes with all other flags (--baseline, --format, --fail-on, --select, --ignore).
smellcheck caches per-file analysis results in .smellcheck-cache/ to skip unchanged files on repeated scans. This is especially useful for pre-commit hooks and editor integrations.
Cache entries are keyed by file content hash, config hash, and smellcheck version — any change invalidates the relevant entry. Cross-file analysis (cyclic imports, duplicate code, etc.) always re-runs since it depends on the full file set.
# Caching is enabled by default — just run normally
smellcheck src/
# Disable caching for a guaranteed fresh scan
smellcheck src/ --no-cache
# Use a custom cache directory
smellcheck src/ --cache-dir /tmp/sc-cache
# Clear all cached results
smellcheck --clear-cacheOld cache entries are not automatically evicted. Run smellcheck --clear-cache periodically or after upgrading to reclaim disk space.
Add .smellcheck-cache/ to your .gitignore. You can also configure caching in pyproject.toml:
[tool.smellcheck]
cache = false # disable caching
cache-dir = ".smellcheck-cache" # custom cache directoryEvery rule is identified by an SC code (e.g. SC701). Use SC codes in --select, --ignore, and # noqa comments.
| SC Code | Pattern | Severity |
|---|---|---|
| SC101 | Setters (half-built objects) | warning |
| SC102 | UPPER_CASE without Final | info |
| SC103 | Unprotected public attributes | info |
| SC104 | Half-built objects (init assigns None) | warning |
| SC105 | Boolean flag parameters | info |
| SC106 | Global mutable state | info |
| SC107 | Sequential IDs | info |
| SC201 | Long functions (>20 lines) | warning |
| SC202 | Generic names (data, result, tmp) | info |
| SC203 | input() in business logic | warning |
| SC204 | Functions returning None or list | info |
| SC205 | Excessive decorators (>3) | info |
| SC206 | Too many parameters (>5) | warning |
| SC207 | CQS violation (query + modify) | info |
| SC208 | Unused function parameters | warning |
| SC209 | Long lambda (>60 chars) | info |
| SC210 | Cyclomatic complexity (>10) | warning |
| SC301 | Extract class (too many methods) | info |
| SC302 | isinstance chains | warning |
| SC303 | Singleton pattern | warning |
| SC304 | Dataclass candidate | info |
| SC305 | Sequential tuple indexing | info |
| SC306 | Lazy class (<2 methods) | info |
| SC307 | Temporary fields | info |
| SC401 | Dead code after return | warning |
| SC402 | Deep nesting (>4 levels) | warning |
| SC403 | Loop + append pattern | info |
| SC404 | Complex boolean expressions | warning |
| SC405 | Boolean control flag in loop | info |
| SC406 | Complex comprehension (>2 generators) | info |
| SC407 | Missing default else branch | info |
| SC501 | Error codes instead of exceptions | warning |
| SC502 | Law of Demeter violation | info |
| SC601 | Magic numbers | info |
| SC602 | Bare except / unused exception variable | error |
| SC603 | String concatenation for multiline | info |
| SC604 | contextlib candidate | info |
| SC605 | Empty catch block | warning |
| SC701 | Mutable default arguments | error |
| SC702 | open() without context manager | warning |
| SC703 | Blocking calls in async functions | warning |
| SC704 | Sync I/O imports in async module | warning |
| SC705 | asyncio.to_thread tech debt hint | info |
| SC Code | Pattern | Description |
|---|---|---|
| SC211 | Feature envy | Function accesses external attributes more than own |
| SC308 | Deep inheritance | Inheritance depth >4 |
| SC309 | Wide hierarchy | >5 direct subclasses |
| SC503 | Cyclic imports | DFS cycle detection |
| SC504 | God modules | >500 lines or >30 top-level definitions |
| SC505 | Shotgun surgery | Function called from >5 different files |
| SC506 | Inappropriate intimacy | >3 bidirectional class references between files |
| SC507 | Speculative generality | Abstract class with no concrete subclasses |
| SC508 | Unstable dependency | Stable module depends on unstable module |
| SC509 | Lazy re-export module | Module only re-exports imported symbols with no logic |
| SC606 | Duplicate functions | AST-normalized hashing across files |
| SC706 | Conflicting concurrency libraries | Monkey-patching libs coexist with asyncio-based libs |
| SC Code | Metric | Threshold |
|---|---|---|
| SC801 | Lack of Cohesion of Methods | >0.8 |
| SC802 | Coupling Between Objects | >8 |
| SC803 | Excessive Fan-Out | >15 |
| SC804 | Response for a Class | >20 |
| SC805 | Middle Man (delegation ratio) | >50% |
See the OO Metrics Interpretation Guide for detailed explanations, real-world examples, and when-to-ignore guidance.
smellcheck runs three analysis passes, each building on the previous one:
.py file is parsed independently. An AST visitor walks the tree and runs ~35 _check_* methods that detect local smells. This is fast and needs no cross-file context.`--diff` tradeoff: When using--diff, only changed files are analyzed. Per-file checks are accurate for those files, but cross-file and OO metric checks may miss issues introduced in unchanged files that depend on the changed ones. For full accuracy, run a periodic full scan without--diff.
Each pattern includes a description, before/after code examples, and trade-offs:
| File | Patterns |
|---|---|
state.md | Immutability, setters, attributes (SC101–SC107) |
functions.md | Extraction, naming, parameters, CQS (SC201–SC210) |
types.md | Classes, reification, polymorphism, nulls (SC301–SC309) |
control.md | Guards, pipelines, conditionals, phases (SC401–SC407) |
architecture.md | DI, singletons, exceptions, delegates (SC501–SC509) |
hygiene.md | Constants, dead code, comments, style (SC601–SC606) |
idioms.md | Context managers, generators, unpacking, async (SC701–SC706) |
metrics.md | OO metrics: cohesion, coupling, fan-out, response, delegation (SC801–SC805) |
| Feature | smellcheck | PyExamine | SMART-Dal | Pyscent |
|---|---|---|---|---|
| Automated detections | 60 | 49 | 31 | 11 |
| Refactoring guidance | 83 patterns | None | None | None |
| Dependencies | 0 (stdlib) | pylint, radon | DesigniteJava | pylint, radon, cohesion |
| Python-specific idioms | Yes | No | No | No |
| Cross-file analysis | Yes | Limited | Yes | No |
| OO metrics | 5 | 19 | 0 | 1 |
| Distribution channels | 4 (pip, GHA, pre-commit, Agent Skills) | 1 | 1 | 1 |
Contributions welcome — see CONTRIBUTING.md for the full guide. The core detector is src/smellcheck/detector.py; add new checks by extending the SmellDetector AST visitor class and adding a cross-file analysis function if needed.
# Development setup
git clone https://github.com/cheickmec/smellcheck.git
cd smellcheck
pip install -e .
pip install pytest
# Run tests
pytest tests/ -v
# Self-check
smellcheck src/smellcheck/MIT
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.