hunter-party-py/smell-hunter-py/SKILL.md
Audit Python code for classic code smells — feature envy, data clumps, shotgun surgery, temporal coupling, comments as deodorant, temporary fields, god modules, mutable default arguments, and class abuse. Use when: reviewing Python code for structural design problems, preparing for a refactor, auditing code after rapid feature development, or hunting for misplaced responsibilities.
npx skillsauth add skyosev/agent-skills smell-hunter-pyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit Python code for code smells — structural patterns that indicate deeper design problems. This covers selected Fowler/Beck smells and Python-specific antipatterns that fall outside the scope of specialized hunters (SOLID, type design, boundaries, invariants, etc.). The goal: data lives where it's used, changes are localized, domain concepts are modeled explicitly, and Python idioms are respected.
Not covered (owned by other hunters): long method / mixed concerns (→ simplicity-hunter), dead code (→ simplicity-hunter / slop-hunter), speculative generality (→ simplicity-hunter), magic numbers (→ doc-hunter), interface pollution (→ simplicity-hunter / solid-hunter), primitive obsession / type design (→ type-hunter). See Operating Constraints for handoff rules.
Code smells are symptoms, not diagnoses. Each finding indicates a likely design problem that warrants investigation. Context determines whether the smell is a genuine issue or an acceptable trade-off.
Smells are symptoms, not diseases. A code smell indicates a probable design problem, not a guaranteed one. Evaluate each smell in context — some are intentional trade-offs. The goal is awareness, not mechanical elimination.
Follow the data. Feature envy and data clumps point to misplaced or undermodeled data. When data and behavior want to be together, let them. When a group of values always travels together, they are a missing type.
One change, one place. Shotgun surgery means a single logical change requires edits across many unrelated files. This is the hallmark of misaligned module boundaries or scattered responsibilities. The fix is cohesion.
Comments should not be deodorant. A comment explaining confusing code is a band-aid over a design problem. The fix is clearer code — better names, extracted functions, simpler structure — not more comments.
Model the domain. Data clumps often indicate missing domain types. A tuple of floats
that represent a coordinate, or a group of parameters that always appear together — these are domain concepts
begging for a dataclass or NamedTuple. (For primitive-to-domain-type promotions like str → NewType, see
type-hunter-py.)
Use the language, not fight it. Python has powerful features — dataclasses, NamedTuples, Protocols, descriptors, context managers, generators — that eliminate entire categories of smells. When the language provides an idiom, use it instead of reimplementing patterns from other languages.
Refactor incrementally. Split by responsibility, not by size. Introduce abstraction only when needed (wait for the second use case). Preserve behavior first — add tests before restructuring.
A function or method that uses more data from another module or class than from its own context.
Signals:
Action: Move the function to the module/class that owns the data it operates on. If it uses data from two types equally, consider whether the shared data should be extracted into its own type.
The same group of parameters or attributes that always appear together across multiple function signatures or type definitions.
Signals:
host, port, scheme or
latitude, longitude, altitude)street, city, zip, country in several
types)start_date, end_date, timezone inside a larger
config)Action: Extract the group into a named dataclass or NamedTuple. Replace the individual parameters/fields with the type. If the group appears only in function signatures, create a parameter type. If it appears in multiple type definitions, extract a shared type.
A single logical change requires edits across many unrelated files or modules.
Signals:
Action: Consolidate the scattered responsibility. If a change to concept X requires touching modules A, B, C, D, and E, then X's logic is spread too thin. Consider:
Functions or operations that must be called in a specific order, but nothing in the API enforces that order.
Signals:
init(), setup(), or configure() methods that must be called before run() or process()build() can be called before required fields are setAction: Redesign the API to make the order implicit:
__init__ to accept all required dependencies — return a fully initialized objectwith statement) to enforce setup/teardown orderingComments that paper over a confusing code structure that should be refactored instead. The smell is not the missing documentation (→ doc-hunter) but the design problem the comment is covering for.
Signals:
Action: Extract the commented block into a function with an intent-revealing name. Replace complex expressions with named variables or helper functions. If the comment explains an external constraint or business rule, it belongs — that's doc-hunter territory, not a smell.
Object attributes or dataclass fields that are meaningful only in certain states or during specific operations — they
are set for one code path and None for all others.
Signals:
temp_result, cached_data, last_error that serve a single transient useAction: Extract the temporary attributes into a separate dataclass used only where needed. If the type represents multiple states, use separate dataclasses per state or a tagged union. For transient computation, use local variables or return values instead of instance attributes.
A single .py file that accumulates unrelated responsibilities, becoming the dumping ground for everything.
Signals:
utils.py, helpers.py, common.py, misc.py with broad scope__init__.py that contains business logic instead of just re-exportsAction: Split by responsibility into focused modules. Each module should have a clear, single purpose. If the
module is truly shared utilities, group by domain concept (e.g., string_utils.py, date_utils.py).
Using mutable objects (lists, dicts, sets) as default argument values — a classic Python footgun.
Signals:
def func(items: list = []) or def func(config: dict = {})__init__ with mutable defaults: def __init__(self, data: list = [])default=[] or default={} (without field(default_factory=list))Action: Use None as default and create the mutable object inside the function:
def func(items: list[str] | None = None) -> None:
items = items if items is not None else []
Or use dataclasses.field(default_factory=list) for dataclass fields.
Using classes where plain functions, modules, or dataclasses would be simpler and more Pythonic.
Signals:
__init__ and one public method (a function in disguise)@staticmethods or @classmethods (a module in disguise)__init__ just assigns parameters to self with no validation or logic__new__ or metaclass (use a module-level instance)Action: Replace with the simpler construct:
Entities or dataclasses that hold data but contain no business logic — all behavior lives in separate "service" or "handler" classes that operate on the data externally.
Signals:
__init__, and no methods that encode business rules or state transitionsUserService that validates, transitions, and mutates User, while User is just a data bag with public fieldsorder.status = "shipped") instead of via a domain method (order.ship())if order.status == "paid") then acts, instead
of the entity deciding (order.can_ship() → bool)Action: Move business logic into the entity. State transitions should be methods on the entity
(order.ship(), user.deactivate()). Validation of entity invariants belongs in __init__ or __post_init__.
Keep services as orchestrators that coordinate between entities, not as the sole location of domain logic.
If the entity is genuinely just a data carrier with no behavior (a DTO, a config object), it's fine as a
plain dataclass — the smell applies when the entity should have behavior but it's been extracted away.
main/master)BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)
Constrain all subsequent scans to the resolved surface.These scans produce candidates only — each match requires manual validation in Phase 3–5 before it becomes a finding. Expect a high false-positive rate from regex heuristics; the value is in surfacing locations to inspect.
For diff/path mode, append the resolved file list ($SCOPE) to each rg command. For codebase mode, omit it.
EXCLUDE='--glob !**/*_test.py --glob !**/test_*.py --glob !**/tests/** --glob !**/venv/** --glob !**/.venv/** --glob !**/dist/** --glob !**/__pycache__/**'
# Feature envy: methods that heavily reference another module's types
# (look for obj.attr.attr patterns across module boundaries — verify manually)
rg --pcre2 '\b[a-z]\w+\.[a-z]\w+\.[a-z]\w+' --type py $EXCLUDE -- $SCOPE
# Data clumps: repeated parameter groups (functions with 4+ params)
rg --pcre2 'def\s+\w+\s*\([^)]{100,}\)' --type py $EXCLUDE -- $SCOPE
# Temporal coupling: init/setup/configure methods
rg --pcre2 '(init|setup|configure|prepare)\s*\(' --type py $EXCLUDE -- $SCOPE
# Mutable default arguments
rg --pcre2 'def\s+\w+\s*\([^)]*=\s*(\[\]|\{\}|set\(\))' --type py $EXCLUDE -- $SCOPE
# God modules (large files)
find . -name '*.py' -not -path '*/venv/*' -not -path '*/.venv/*' -exec wc -l {} + | sort -rn | head -20
# Classes (then evaluate for class abuse)
rg 'class\s+\w+' --type py $EXCLUDE -- $SCOPE
# Singleton pattern
rg '__new__\s*\(|_instance' --type py $EXCLUDE -- $SCOPE
# Comments as deodorant: multi-line comment blocks before code (inspect for "what" vs "why")
rg -B1 -A1 '^\s*# ' --type py $EXCLUDE -- $SCOPE | head -200
# Shotgun surgery: per-commit co-occurrence (see Phase 4)
For each function with cross-module data access:
For each function with 4+ parameters:
Shotgun surgery is detected through per-commit co-change analysis, not raw file churn. High churn on a single file is not shotgun surgery — the signal is many unrelated files changing together for a single logical change.
# Per-commit file sets: show which files change together in each commit
git log --pretty=format:'--- %h %s' --name-only -30 | head -200
# Directory co-occurrence: for each commit, list distinct directories touched
git log --pretty=format:'COMMIT' --name-only -50 | awk '
/^COMMIT/ { if (NR>1) { for (d in dirs) printf "%s ", d; print "" } delete dirs; next }
/\// { sub(/\/[^\/]*$/, ""); dirs[$0]=1 }
' | sort | uniq -c | sort -rn | head -20
For each commit that touches 4+ directories, ask: was this a single logical change scattered across unrelated modules, or a legitimate cross-cutting concern? Look for patterns: the same directory set appearing in multiple commits suggests structural coupling.
For each class:
For each mutable default argument:
For each god module:
Save as YYYY-MM-DD-smell-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs folder
exists).
# Smell Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list}
## Findings
### Feature Envy
| # | Location | Function | Envied Type | Own Data Used | Foreign Data Used | Evidence | Action |
| - | -------- | -------- | ----------- | ------------- | ----------------- | -------- | ------ |
| 1 | file:line | `format_order()` | `billing.Invoice` | 0 attributes | 5 attributes | `inv.total + inv.tax...` | Move to billing module |
### Data Clumps
| # | Locations | Parameters/Attributes | Evidence | Suggested Type | Action |
| - | --------- | -------------------- | -------- | -------------- | ------ |
| 1 | file:line, file:line, file:line | `host, port, scheme` | 3 func signatures | `Endpoint` dataclass | Extract type |
### Shotgun Surgery
| # | Concept | Files Touched | Modules Touched | Action |
| - | ------- | ------------- | --------------- | ------ |
| 1 | "Add new payment method" | 8 files | 5 modules | Consolidate payment logic |
### Temporal Coupling
| # | Location | Class/Object | Required Order | Action |
| - | -------- | ------------ | -------------- | ------ |
| 1 | file:line | `Server` | `init()` → `start()` | Require deps in `__init__` |
### Comments as Deodorant
| # | Location | Comment | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | `# Parse and validate the user input` | Extract `parse_and_validate_input()` |
### Temporary Field
| # | Location | Type | Attribute | Used In | Action |
| - | -------- | ---- | --------- | ------- | ------ |
| 1 | file:line | `Processor` | `last_result` | `process()` only | Use separate dataclass or local var |
### God Module
| # | Location | Module | Lines | Responsibilities | Action |
| - | -------- | ------ | ----- | ---------------- | ------ |
| 1 | file.py | `utils.py` | 800 | string ops + date ops + I/O helpers | Split by domain |
### Mutable Default Arguments
| # | Location | Function | Default | Risk | Action |
| - | -------- | -------- | ------- | ---- | ------ |
| 1 | file:line | `add_item(items=[])` | `[]` | State persistence between calls | Use `None` sentinel |
### Class Abuse
| # | Location | Class | Methods | State | Action |
| - | -------- | ----- | ------- | ----- | ------ |
| 1 | file:line | `UserService` | 1 public | none | Replace with exported function |
## Recommendations (Priority Order)
1. **Must-fix**: {mutable default args, data clumps with 5+ occurrences, feature envy in critical paths}
2. **Should-fix**: {feature envy, shotgun surgery patterns, god modules, temporal coupling}
3. **Consider**: {class abuse, comments as deodorant, temporary fields}
file/path.py:line with the exact code.development
Transforms vague feature ideas into precise, codebase-grounded technical requirements. Use when requirements are ambiguous/incomplete, the user struggles to describe behavior, terminology is unclear, or multiple concepts are mixed. Output is a requirements spec—NOT an implementation plan.
tools
Audit TypeScript type definitions for design debt — duplicated shapes, missing derivations, over-engineered generics, under-constrained type parameters, reinvented utility types, and disorganized type architecture. Type structure and maintainability, not type enforcement. Use when: reviewing type definitions for maintainability, reducing type duplication, simplifying over-engineered type-level logic, or reorganizing type architecture after growth.
development
Audit TypeScript test code for quality gaps — missing coverage on critical paths, brittle tests coupled to implementation, over-mocking, assertion-free tests, missing edge cases, and duplicated test setup. Focuses on test effectiveness, not production code structure. Use when: reviewing TypeScript test suites for reliability, reducing false-positive test failures, improving coverage of critical business logic, or cleaning up test debt.
tools
Audit TypeScript class and interface design for SOLID violations — god classes, rigid extension points, broken substitutability, fat interfaces, and concrete dependency chains. Focuses on responsibility assignment and abstraction fitness. Use when: reviewing class hierarchies, preparing for extension with new variants, reducing coupling between services, or improving testability of class-heavy code.