Skip to main content

Python code smell detector -- 83 refactoring patterns, 56 AST checks, zero dependencies

Project description

smellcheck logo

smellcheck

Python Code Smell Detector & Refactoring Guide
83 refactoring patterns · 56 automated AST checks · zero dependencies

PyPI Python CI Downloads pre-commit License

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 →

Installation

pip

pip install smellcheck

smellcheck src/
smellcheck myfile.py --format json
smellcheck src/ --min-severity warning --fail-on warning

Also available as a GitHub Action, pre-commit hook, SARIF/Code Scanning integration, Agent Skills plugin, and Cursor native plugin for Claude Code, Cursor, Copilot, Gemini CLI, and more.

Full installation guide →

Usage

# 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

Configuration

smellcheck 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.

Config inheritance (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).

Suppression

Per-line

Add # noqa: SC701 to a line to suppress that check on that line:

def foo(x=[]):  # noqa: SC701
    return x

Use # noqa (no codes) to suppress all findings on that line. Multiple codes: # noqa: SC601,SC202

Block-level

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, SC305

Disable all checks for a range:

# smellcheck: disable-all
# ... everything in this range is suppressed ...
# smellcheck: enable-all

File-level

Suppress checks for an entire file (place at top of file):

# smellcheck: disable-file SC301, SC305

Use # smellcheck: disable-file (no codes) to suppress all checks for the entire file.

Scope rules

  • 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 once
  • disable-file applies to the entire file
  • Per-line # noqa still works alongside block directives
  • Block directives do not affect cross-file findings (use per-file-ignores in config instead)

Baseline

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 automatically

Fingerprints 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.

Diff-Aware Scanning

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-only

In CI, this keeps PR feedback fast and relevant:

- uses: cheickmec/smellcheck@v0
  with:
    diff: origin/main
    fail-on: warning

Cross-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).

Caching

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-cache

Old 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 directory

Features

  • 56 automated smell checks -- per-file AST analysis, cross-file dependency analysis, and OO metrics
  • 83 refactoring patterns -- numbered catalog with before/after examples, trade-offs, and severity levels
  • Zero dependencies -- stdlib-only, runs on any Python 3.10+ installation
  • Multiple output formats -- text (terminal), JSON (machine-readable), GitHub annotations (CI), SARIF 2.1.0 (Code Scanning), JUnit XML (Jenkins/GitLab/CircleCI), GitLab CodeClimate (MR quality widget)
  • Configurable -- pyproject.toml config, inline suppression, CLI overrides
  • Baseline support -- adopt incrementally by suppressing existing findings and only failing on new ones
  • File-level caching -- content-hash based caching skips unchanged files for fast repeated scans
  • Multiple distribution channels -- pip, GitHub Action, pre-commit, Agent Skills (full list)

Detected Patterns

Every rule is identified by an SC code (e.g. SC701). Use SC codes in --select, --ignore, and # noqa comments.

Per-File (41 checks)

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

Cross-File (10 checks)

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
SC606 Duplicate functions AST-normalized hashing across files

OO Metrics (5 checks)

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%

Refactoring Reference Files

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–SC508)
hygiene.md Constants, dead code, comments, style (SC601–SC606)
idioms.md Context managers, generators, unpacking, async (SC701–SC703)
metrics.md OO metrics: cohesion, coupling, fan-out, response, delegation (SC801–SC805)

How It Compares

Feature smellcheck PyExamine SMART-Dal Pyscent
Automated detections 56 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

Contributing

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/

License

MIT

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

smellcheck-0.3.7.tar.gz (1.7 MB view details)

Uploaded Source

Built Distribution

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

smellcheck-0.3.7-py3-none-any.whl (50.5 kB view details)

Uploaded Python 3

File details

Details for the file smellcheck-0.3.7.tar.gz.

File metadata

  • Download URL: smellcheck-0.3.7.tar.gz
  • Upload date:
  • Size: 1.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for smellcheck-0.3.7.tar.gz
Algorithm Hash digest
SHA256 8bbd60821d246dd371694a57cd83e7eefd7c707817ac596a4b7a9f4bedebe0df
MD5 2b6bdb9a636c17cb63a43c1c0de92026
BLAKE2b-256 1a78aed76ad0fb2233f5a67455c176ee7a9e6557a65cae27aa7cca488bffa323

See more details on using hashes here.

Provenance

The following attestation bundles were made for smellcheck-0.3.7.tar.gz:

Publisher: release-please.yml on cheickmec/smellcheck

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

File details

Details for the file smellcheck-0.3.7-py3-none-any.whl.

File metadata

  • Download URL: smellcheck-0.3.7-py3-none-any.whl
  • Upload date:
  • Size: 50.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for smellcheck-0.3.7-py3-none-any.whl
Algorithm Hash digest
SHA256 b1869b057b2081825049a0860c50ce7240d3ed41403822f7f9ac1bb2d9f55037
MD5 33640d9f1f4da3fd5cec07865e5b408e
BLAKE2b-256 8604ab98e87a6469cd7c64bd219745c249b9fdc91edcd57f23c22695f3557966

See more details on using hashes here.

Provenance

The following attestation bundles were made for smellcheck-0.3.7-py3-none-any.whl:

Publisher: release-please.yml on cheickmec/smellcheck

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