pip install no-slop| Component | Purpose | Auto-registers |
|---|---|---|
no_slop.mypy_plugin |
Type-based redundancy checks | No (needs config) |
no_slop.flake8_plugin |
Style checks (ASCII art, emojis) | Yes |
no-slop-unused-defaults |
Whole-program default analysis | CLI tool |
| Code | Description |
|---|---|
slop-isinstance |
isinstance(x, T) when x: T is guaranteed |
slop-issubclass |
issubclass(C, T) when C is subclass of T |
slop-hasattr |
hasattr(obj, "attr") when type guarantees attr |
slop-getattr |
getattr(obj, "attr") when attr guaranteed |
slop-setattr |
setattr(obj, "attr", val) when attr guaranteed |
slop-delattr |
delattr(obj, "attr") when attr guaranteed |
slop-callable |
callable(f) when f is typed as Callable |
slop-dict-get |
TypedDict.get("key", default) when key is required |
slop-any-check |
Reflection on Any/object/untyped values |
| Code | Description |
|---|---|
SLP020 |
Excessive module docstring or leading comments |
SLP021 |
ASCII art (box drawing, block characters) |
SLP022 |
Emojis in code |
SLP023 |
Local imports (imports inside functions) |
SLP030 |
Conversational residue (e.g., "Here is the code") |
SLP031 |
Obvious comments (e.g., "# Import modules") |
SLP032 |
Generic variable names in function signature |
SLP033 |
Placeholder body (pass/ellipsis/NotImplemented) |
SLP034 |
Orphaned placeholder TODO/FIXME without context |
SLP035 |
Debug artifacts (print/breakpoint/set_trace) |
SLP036 |
Cross-language API on literal or annotated type |
SLP037 |
Unresolvable import (not stdlib/local/declared) |
SLP038 |
Hedging comments in code/docstrings |
SLP039 |
Single-method class wrapper |
SLP040 |
Over-nested or oversized function |
SLP401 |
obj.__dict__.get() instead of direct access |
SLP402 |
obj.__dict__[key] instead of obj.attr |
SLP403 |
obj.__dict__[key] = val instead of obj.attr= |
SLP404 |
obj.__dict__.update() instead of assignment |
SLP405 |
vars(obj).get() instead of direct access |
SLP406 |
vars(obj)[key] = val instead of assignment |
SLP407 |
asdict(obj).get() instead of obj.attr |
SLP408 |
.get("key", default) when key is in dict |
SLP501 |
Unnecessary else after return/raise/break/continue |
SLP502 |
Redundant boolean comparison (== True/False/None, len(x) == 0) |
SLP503 |
Unnecessary pass in non-empty block |
SLP504 |
Bare except or except Exception: pass |
SLP505 |
Mutable default argument (def f(x=[])) |
SLP506 |
f-string with no placeholders |
SLP507 |
Redundant list()/dict() wrapping |
SLP508 |
Unnecessary .keys() in iteration |
SLP509 |
Explicit return None at end of function |
SLP510 |
type(x) == T instead of isinstance(x, T) |
SLP601 |
Redundant loop guarding (if x: for y in x:) |
SLP602 |
Unpythonic loop (for i in range(len(x)) with indexing) |
| Code | Description |
|---|---|
SLP010 |
Default parameter never used by any call site |
# pyproject.toml
[tool.mypy]
plugins = ["no_slop.mypy_plugin"]# mypy.ini
[mypy]
plugins = no_slop.mypy_pluginAuto-registers on install. No configuration needed.
def process(user: User) -> str:
if isinstance(user, User): # Redundant - user is always User
return user.name
return ""def check(cls: type[User]) -> bool:
return issubclass(cls, User) # Redundant - cls is always type[User]def get_name(user: User) -> str:
if hasattr(user, "name"): # Redundant - User always has 'name'
return user.name
return ""def get_email(user: User) -> str:
return getattr(user, "email", "none") # Redundant - User always has 'email'
# Also flags: getattr(user, "email") without defaultdef set_name(user: User, name: str) -> None:
setattr(user, "name", name) # Redundant - use user.name = namedef clear_name(user: User) -> None:
delattr(user, "name") # Redundant - use del user.namedef invoke(func: Callable[[], int]) -> int:
if callable(func): # Redundant - func is Callable
return func()
return 0from typing import TypedDict
class Config(TypedDict):
host: str # Required key
def get_host(config: Config) -> str:
return config.get("host", "default") # Redundant - host always exists
# Use: config["host"]def bad(data): # No type annotation
if isinstance(data, dict): # Add types instead of runtime checks
return data.get("key")def process(data: list[int], multiplier: int = 1) -> list[int]:
return [x * multiplier for x in data]
# All calls provide explicit multiplier - default is dead code
process([1, 2], 2)
process([3, 4], 3)def fetch_data(url: str) -> dict:
import requests # Bad - move to module level
return requests.get(url).json()Imports inside functions:
- Hide dependencies
- Run on every call (performance)
- Make code harder to analyze
Exception: if TYPE_CHECKING: blocks are allowed.
# Here is the updated code
def process():
# As an AI model I suggest
passRemove these chat artifacts.
# Import modules
import os
# Define function
def calculate():
passComments should explain why, not what.
def process_data(data, val): # Bad
pass
def process_user(user_profile, score): # Good
passAvoid data, res, val, item, obj, content, info, result in function signatures.
from dataclasses import dataclass, asdict
@dataclass
class User:
name: str
user = User("John")
# SLP401: __dict__.get()
user.__dict__.get("name", "") # Bad - use user.name or getattr()
# SLP402: __dict__[key] read
user.__dict__["name"] # Bad - use user.name
# SLP403: __dict__[key] write
user.__dict__["name"] = "Jane" # Bad - use user.name = "Jane"
# SLP404: __dict__.update()
user.__dict__.update({"name": "Jane"}) # Bad - use user.name = "Jane"
# SLP405: vars().get()
vars(user).get("name", "") # Bad - use user.name or getattr()
# SLP406: vars()[key] write
vars(user)["name"] = "Jane" # Bad - use user.name = "Jane"
# SLP407: asdict().get()
asdict(user).get("name", "") # Bad - use user.name directlyThese patterns bypass direct attribute access and are almost always unnecessary.
# Inline - key is in the literal
x = {"host": "localhost"}.get("host", "default") # Bad
x = dict(host="localhost").get("host", "default") # Bad
# Variable - key was just defined
config = {"host": "localhost", "port": 8080}
host = config.get("host", "default") # Bad - use config["host"]
# OK - no default
host = config.get("host")
# OK - key not in original dict
timeout = config.get("timeout", 30)
# OK - key might have been removed
config.pop("host")
host = config.get("host", "default")When you define a dict with known keys, using .get("key", default) on those keys is redundant - the key is guaranteed to exist.
def process(x: int) -> str:
if x < 0:
return "negative"
else:
# This else is unnecessary - can be dedented
log("processing positive")
return "positive"Fix: Remove the else and dedent the code.
def process(x: int) -> str:
if x < 0:
return "negative"
log("processing positive")
return "positive"Note: Single-statement else blocks (e.g., if x: return 1 else: return 0) are NOT flagged as they're a valid, clear pattern.
# Bad
if enabled == True: # Use: if enabled
if enabled == False: # Use: if not enabled
if value == None: # Use: if value is None
if value != None: # Use: if value is not None
if len(items) == 0: # Use: if not items
if len(items) > 0: # Use: if items# Bad - pass is unnecessary when block has other statements
def foo():
x = 1
pass # Remove this
# OK - pass is the only statement
class Empty:
pass# Bad - catches all exceptions including KeyboardInterrupt
try:
risky()
except:
pass
# Bad - swallows all exceptions silently
try:
risky()
except Exception:
pass
# OK - handles specific exception
try:
risky()
except ValueError:
log("value error")
# OK - at least logs the exception
try:
risky()
except Exception:
log_error()# Bad - list is created once and shared between calls
def append_item(items=[]):
items.append(1)
return items
# Bad - same issue with dict
def update_config(config={}):
config["key"] = "value"
return config
# Good - use None and initialize in body
def append_item(items=None):
if items is None:
items = []
items.append(1)
return items# Bad - no reason to use f-string
message = f"Hello World"
# Good - use regular string
message = "Hello World"
# Good - f-string with actual placeholders
name = "World"
message = f"Hello {name}"# Bad - use [] instead
items = list()
# Bad - list literal inside list()
items = list([1, 2, 3])
# Bad - use {} instead
config = dict()
# Bad - dict literal inside dict()
config = dict({"a": 1})
# OK - converting iterator to list
items = list(range(10))
# OK - dict with keyword args
config = dict(a=1, b=2)# Bad - .keys() is redundant in iteration
for key in data.keys():
print(key)
# Good - iterate directly
for key in data:
print(key)
# OK - .keys() is needed for set operations
common = data.keys() & other.keys()# Bad - explicit return None at end is redundant
def process(x):
print(x)
return None
# Bad - bare return at end is also redundant
def process(x):
print(x)
return
# Good - just omit the return
def process(x):
print(x)
# OK - return None in middle for early exit
def process(x):
if x < 0:
return None
return x * 2# Bad - doesn't handle subclasses
if type(x) == int:
process(x)
# Good - handles subclasses correctly
if isinstance(x, int):
process(x)# Bad - if check is redundant because loop won't run if empty
if items:
for item in items:
process(item)
# Good
for item in items:
process(item)# Bad - C-style iteration
for i in range(len(items)):
print(items[i])
# Good - direct iteration
for item in items:
print(item)
# Good - if index is needed
for i, item in enumerate(items):
print(i, item)if hasattr(user, "name"): # type: ignore[slop-hasattr]
...x = "hello" # noqa: SLP022
x = "test" # noqa: SLP021, SLP022
x = "test" # noqa
def lazy_import():
import heavy_module # noqa: SLP023
# File-level (first 10 lines)
# slop: ignore-file
# slop: ignore-file[SLP021, SLP022]no-slop-unused-defaults /path/to/project
no-slop-unused-defaults . --json
no-slop-unused-defaults . --min-calls 3
no-slop-unused-defaults . --quiet| Option | Description |
|---|---|
--json |
JSON output |
--min-calls N |
Min call sites to report (default: 1) |
--quiet |
Suppress progress |