The type checker was satisfied.
That was the problem.
anti-slop-py is a standalone, zero-dependency Python linter that rejects
low-evidence patterns — the escape hatches a coding agent reaches for instead of naming
a contract. A type checker permits Any, cast, and
unexplained # type: ignore by construction: they are legal holes in its own
type system. anti-slop bans exactly those holes.
Real output, trimmed at the ellipses. Every diagnostic ends in a recipe, not a prohibition — written to be executed by the same agent that just wrote the slop.
anti-slop catches the moment an agent silences the checker without adding
evidence. The stricter the type checker, the more that control matters: the moment an agent
hits a type error, cast / ignore is the first thing it reaches for —
and that is exactly where these rules fire.
Fifteen rules, two tiers
Analysis is purely syntactic — stdlib ast and tokenize,
no type checker in the loop. Every rule declares its tier and confidence as machine-readable
metadata; that is what --list-rules, --explain, and the presets read.
Escape-hatch
10 rulesConstructs whose main effect is to discard evidence the type checker already had. Near-universal — enable them first, everywhere.
Parameters must name a domain type, not the Any escape hatch.
Return annotations must name a domain type, not Any.
Type aliases must name a domain type, not hide Any behind a name.
A cast call must not take another cast call as its value.
A dict spread’s source must not be a ternary with an empty dict on either branch.
Dispatch must not go through a namespace subscript or an attrgetter/methodcaller built from a runtime name.
A literal value’s annotation must not widen it to Any or object.
Dict-like value types must name a domain type, not Any/object.
A value must not be widened to Any/object and then cast back to a
narrow type: the evidence was already there before the widening.
Every cast and every type-checker suppression must state its verified
invariant in a # SAFETY: comment, and every suppression must name a code.
Architectural
5 rulesExecutable policy for the places ordinary linters stay silent — and a policy some teams reject wholesale. Every rule takes a per-rule level; every finding is suppressible by rule id. The defaults are a posture, not a ceiling.
isinstance()/issubclass() must live inside a
TypeGuard/TypeIs function, not branch ad hoc.
Inject dependencies through real seams instead of patching modules.
Parameters must name a domain type, not the object top type.
A declared name must not encode a banned structural term.
getattr()/setattr()/delattr() must not stand in
for direct attribute access or unparsed dynamic access.
Four more rules ship in the opt-in fastapi group — framework
policy stays out of the core. And this repository practices what it configures: its own
pyproject.toml sets no-adhoc-isinstance to off, because
an AST analyzer’s domain objects are ast nodes. That is the configuration
model working as intended, not an exception to it.
Vendored, not depended upon
The copy is meant to be read and adjusted to your team’s standards —
in vendored mode nothing is installed at all. No virtualenv, no PYTHONPATH:
running the directory lints the repository on its own interpreter.
$ npx skills add TinyFrontier/anti-slop-py --skill install-anti-slop-py # then ask your agent to install anti-slop in the current repository $ python tools/anti_slop --list-rules $ python -m anti_slop review --base origin/main # only what this change touched, read back as a review $
The install skill inspects the target repository, copies the linter into
tools/anti_slop/, merges [tool.anti-slop], wires up pre-commit and CI,
and hands Ruff’s duplicate rules over to their anti-slop equivalents.