anti-slop-py
Opinionated lint rules that reject low-evidence, low-signal Python patterns. A port of anti-slop (Oxlint/TypeScript) to the Python ecosystem.
Vendor this project instead of pinning it as a dependency. Copy the rules into your repository, read them, and change them until they say what your team actually believes. The bundled agent skill does the first copy and wiring; after that the files are yours to maintain.
Architecture: three layers
Ruff still has no API for custom rules, so anti-slop-py can't be a single plugin. It's one opinionated configuration spread across three tools, each covering the part it already handles well:
- Ruff (
configs/ruff-anti-slop.toml), for the rules that already exist:ANN401(noAnyin signatures),PGH003(no blankettype: ignore),B009/B010(no constant-namegetattr/setattr), andTID251with a banned-API list that includesunittest.mock.patch. - mypy strict flags (
configs/mypy-anti-slop.ini).disallow_any_explicitand friends catchAnyin the places Ruff can't see: nested generics, aliases, plain variables. - A Flake8 plugin (
src/anti_slop/, vendored as a local plugin) carrying the ten customASPrules below. It runs next to Ruff, scoped withselect = ASP.
Install with an agent skill
Clone the repository and point your coding agent at the bundled skill:
git clone https://github.com/infoslack/anti-slop-py
Then, from the target repository, ask the agent to install anti-slop-py following <clone>/skills/install-anti-slop-py/SKILL.md. The skill copies the plugin to tools/flake8/anti_slop/, registers it as a Flake8 local plugin, merges the Ruff and mypy layers into whatever configuration already exists, and validates the result. When pydantic or fastapi shows up as a direct dependency, it also enables the matching framework group.
If you have Node available, the skills.sh CLI does the fetch in one step and registers the skill with your agent:
npx skills add infoslack/anti-slop-py --skill install-anti-slop-py
Claude Code users can install it as a plugin instead:
/plugin marketplace add infoslack/anti-slop-py
/plugin install anti-slop-py@anti-slop-py
Scan without installing
The rules also run straight from a clone, reporting findings without adding a single file to the target project. Flake8 resolves the plugin relative to the config file, so from the target repository:
flake8 --config <clone>/configs/flake8-scan.ini src tests
Pair it with CLI-only flags for the other layers (ruff check --extend-select ANN401,PGH003,B009,B010, mypy --disallow-any-explicit) and nothing in the repo changes. With the package published, the clone becomes optional too: uvx --with anti-slop-py flake8 --enable-extensions=ASD,ASF src tests. This is the audit mode; the two installation paths below are for repositories that want the rules enforced in CI for every developer.
Install as a package
The lighter enforcement path:
uv add --dev anti-slop-py
The entry points register the ASP rules with Flake8 automatically; a fresh flake8 src tests already reports them. The framework groups ship off by default and turn on per project:
[flake8]
select = ASP
# with pydantic/fastapi as direct dependencies:
# select = ASP,ASD,ASF
# enable-extensions = ASD,ASF
Then merge configs/ruff-anti-slop.toml into your Ruff configuration and configs/mypy-anti-slop.ini into your mypy configuration (both ship inside the sdist as reference).
Vendored installation
For teams that want to own and edit the rules rather than track releases: copy src/anti_slop/ into the target repository (say, at tools/flake8/anti_slop/), install flake8 as a development dependency, and register the local plugin in .flake8, since Flake8 doesn't read pyproject.toml:
[flake8]
select = ASP
[flake8:local-plugins]
extension =
ASP = anti_slop.checker:AntiSlopChecker
paths =
./tools/flake8
The Ruff and mypy layers merge the same way as in the package path. Avoid running the vendored copy and the installed package at once; each finding gets reported twice.
Rules
Each rule documents which original anti-slop rule it ports and names the ready-made tool covering the complementary half.
| Code | Ports | Rejects |
|---|---|---|
ASP001 |
require-safety-comment-for-type-assertion |
typing.cast(...) or # type: ignore with no # SAFETY: comment documenting the checked invariant |
ASP002 |
no-runtime-typeof |
ad hoc isinstance/hasattr/type(x) is narrowing; parse at the boundary instead. --anti-slop-allow-typeguards permits checks inside TypeGuard/TypeIs functions |
ASP003 |
no-module-mocking |
mock.patch(...), mocker.patch(...), monkeypatch.setattr(...); inject dependencies through real seams |
ASP004 |
no-chained-type-assertions |
cast(User, cast(object, value)) |
ASP005 |
no-known-value-widening |
handlers: dict[str, Handler] = {"start": ...}, where the broad annotation throws away known keys; use inference, Final, or a TypedDict |
ASP006 |
no-reflect-get / no-reflect-apply |
getattr(owner, dynamic_name); Ruff B009/B010 cover the constant-name forms |
ASP007 |
no-shape-in-symbol-names |
shape in class, function, parameter, or variable names |
ASP008 |
no-object-parameters / no-unknown-* |
parameters or returns typed bare object, Python's safe top type; dunder methods and cause parameters are exempt |
ASP009 |
no-unsafe-dictionary-type |
dict[str, Any], Mapping[str, object], and friends; define a TypedDict, dataclass, or model |
ASP010 |
no-unknown-type-aliases |
type ExternalValue = Any plus the TypeAlias and bare-assignment spellings |
Not ported: no-conditional-empty-object-spread, because the JS idiom barely exists in Python, and no-widen-then-assert, which needs local flow analysis and stays on the roadmap. The Effect rule no-service-constructor-imports maps to import-linter contracts rather than a lint rule.
Framework groups (opt-in)
Framework policy lives in separate rule groups, the same split the original makes with anti-slop-effect. The groups are off_by_default Flake8 plugins: enable one only when its framework is a direct dependency, with enable-extensions = ASD,ASF next to select (package path) or together with the extension lines in [flake8:local-plugins] (vendored path). Each group repeats the three-layer split, with ready-made rules first and custom ASD/ASF rules only for the gaps.
Pydantic (ASD)
Ready-made layer: flake8-pydantic for PYD hygiene rules, plus the official Pydantic mypy plugin (configs/mypy-anti-slop-pydantic.ini).
| Code | Rejects |
|---|---|
ASD001 |
model_construct() or construct() without a # SAFETY: comment. Both skip validation; the docs allow them only for data you already trust |
ASD002 |
extra="allow" in ConfigDict or a legacy class Config, which stores unvalidated keys without a contract |
ASD003 |
model fields typed Any, bare or nested (list[Any]), opting the field out of validation |
ASD004 |
f(**model.model_dump()) and x: dict[...] = model.model_dump(): re-widening a validated model into an untyped dict, the Pydantic version of no-widen-then-assert |
ASD005 |
TypeAdapter(...) inside a function; official performance guidance says build it once at module scope |
FastAPI (ASF)
Ready-made layer: Ruff FAST (redundant response_model, non-Annotated Depends, unused path params) and ASYNC (blocking I/O inside async routes), via configs/ruff-anti-slop-fastapi.toml.
| Code | Rejects |
|---|---|
ASF001 |
endpoints with no typed response contract: no return annotation and no response_model, or a return annotated Any/dict |
ASF002 |
await request.json(), .body(), or .form() inside endpoints; declare a body model parameter and let the framework parse at the boundary |
ASF003 |
JSONResponse(content={...}) with a dict literal inside endpoints; return a model. Exception handlers and middleware stay unflagged |
Detection is syntactic. Models are found by base-class name (same-module subclasses included), endpoints by @<receiver>.<http-verb>(...) decorators; cross-module inheritance and aliased imports are known misses.
Development
uv run --group dev pytest
uv run flake8 src tests scripts # the project's own rules, on itself
uv run ruff check src tests scripts
uv run mypy
The repo dogfoods all three layers on its own source through the installed entry points; CI enforces it. One documented deviation lives in .flake8: ASP002 targets application boundaries, and an AST linter discriminating the closed ast.* sum type is that boundary, so the rule does not apply to this codebase.
src/ is canonical. After changing production source, run scripts/sync_skill_assets.py so the skill's bundled copy stays identical; --check verifies without writing. Releases: bump __version__ in src/anti_slop/__init__.py, tag v*, and the publish workflow ships to PyPI via trusted publishing.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file anti_slop_py-0.1.0.tar.gz.
File metadata
- Download URL: anti_slop_py-0.1.0.tar.gz
- Upload date:
- Size: 21.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
912b71cae2185d26eb9c54e5ee592674b58bfad66b42df05869b8a52aabdff12
|
|
| MD5 |
272cc073f2eb359188db9887ce04a40d
|
|
| BLAKE2b-256 |
625f05761cf9c7ae37d192b4ee6130c7c0b6bb191c1001cd6ddb81c66d05c629
|
Provenance
The following attestation bundles were made for anti_slop_py-0.1.0.tar.gz:
Publisher:
publish.yml on infoslack/anti-slop-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anti_slop_py-0.1.0.tar.gz -
Subject digest:
912b71cae2185d26eb9c54e5ee592674b58bfad66b42df05869b8a52aabdff12 - Sigstore transparency entry: 2567903593
- Sigstore integration time:
-
Permalink:
infoslack/anti-slop-py@a703a80c9dd1a3abee7b4910ec9fa9ff05acf414 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/infoslack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a703a80c9dd1a3abee7b4910ec9fa9ff05acf414 -
Trigger Event:
push
-
Statement type:
File details
Details for the file anti_slop_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: anti_slop_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92870885a34b029f4767eb7cd9cdc80c135d8f1ef521e35aa33a48fd3cd01b7e
|
|
| MD5 |
697fb8d2d2d38496c9d06af2ce280c72
|
|
| BLAKE2b-256 |
cb7b2bcf9d2f5904f51bb3dc93489a53745d3231b7813552b51774a1afd08896
|
Provenance
The following attestation bundles were made for anti_slop_py-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on infoslack/anti-slop-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anti_slop_py-0.1.0-py3-none-any.whl -
Subject digest:
92870885a34b029f4767eb7cd9cdc80c135d8f1ef521e35aa33a48fd3cd01b7e - Sigstore transparency entry: 2567903601
- Sigstore integration time:
-
Permalink:
infoslack/anti-slop-py@a703a80c9dd1a3abee7b4910ec9fa9ff05acf414 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/infoslack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a703a80c9dd1a3abee7b4910ec9fa9ff05acf414 -
Trigger Event:
push
-
Statement type: