Skip to main content

anti-slop-py

skills.sh

Opinionated lint rules that reject low-evidence, low-signal Python patterns. A port of anti-slop (Oxlint/TypeScript) to the Python ecosystem.

Use it three ways: scan a project without adding a single file to it, install the PyPI package for CI enforcement, or vendor the rules into your repository and change them until they say what your team actually believes. The bundled agent skill wires up whichever mode you pick.

What it catches

Two snippets straight out of an AI assistant's comfort zone. First, a "typed" loader that proves nothing:

from typing import Any, cast


def load_user(payload: dict[str, Any]) -> dict[str, Any]:
    if isinstance(payload.get("id"), str):
        return cast(dict[str, Any], payload)
    raise ValueError("bad payload")
loader.py:4:24: ASP009 dictionary contract with 'Any'/'object' values; define a TypedDict, dataclass, or model
loader.py:4:43: ASP009 dictionary contract with 'Any'/'object' values; define a TypedDict, dataclass, or model
loader.py:5:8: ASP002 ad hoc runtime narrowing; parse input at the boundary or move the check into a TypeGuard/TypeIs function
loader.py:6:16: ASP001 type assertion requires a '# SAFETY:' comment on the same or preceding line documenting the checked invariant
loader.py:6:21: ASP009 dictionary contract with 'Any'/'object' values; define a TypedDict, dataclass, or model

The function checks one key, asserts the rest into existence, and every caller inherits a dict that could hold anything. The shape the rules push toward instead:

from pydantic import BaseModel


class User(BaseModel):
    id: str
    name: str


def load_user(payload: bytes) -> User:
    return User.model_validate_json(payload)

One parse at the boundary, evidence everywhere after it. A typo in a field access is now a type error instead of a None in production, and all five findings disappear because the code stopped needing Any, isinstance, and cast at all.

Second, a FastAPI endpoint with its contract buried in the implementation (framework group enabled):

@app.post("/orders")
async def create_order(request: Request):
    data = await request.json()
    order = save_order(data)
    return JSONResponse(content={"id": order.id, "status": "created"})
orders.py:2:1: ASF001 endpoint without a typed response contract; annotate the return with a model or set response_model
orders.py:3:18: ASF002 manual request parsing; declare a body model parameter so FastAPI parses and validates at the boundary
orders.py:5:12: ASF003 ad hoc dict response; return a response model instead of hand-built JSON

After:

@app.post("/orders")
async def create_order(order_in: OrderIn) -> OrderOut:
    order = save_order(order_in)
    return OrderOut(id=order.id, status="created")

FastAPI now validates the request, serializes the response, and publishes both shapes in the OpenAPI schema; none of that existed in the first version. Both outputs above are real runs of the published package (uvx --with anti-slop-py flake8), not mockups.

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:

  1. Ruff (configs/ruff-anti-slop.toml), for the rules that already exist: ANN401 (no Any in signatures), PGH003 (no blanket type: ignore), B009/B010 (no constant-name getattr/setattr), and TID251 with a banned-API list that includes unittest.mock.patch.
  2. mypy strict flags (configs/mypy-anti-slop.ini). disallow_any_explicit and friends catch Any in the places Ruff can't see: nested generics, aliases, plain variables.
  3. A Flake8 plugin (src/anti_slop/, vendored as a local plugin) carrying the ten custom ASP rules below. It runs next to Ruff, scoped with select = 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, from PyPI:

uv add --dev anti-slop-py
# or: pip install 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).

Running in CI

With the package in the dev dependencies, the whole team and the CI run the same rules. A minimal GitHub Actions job:

name: lint
on: [push, pull_request]

jobs:
  anti-slop:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv sync
      - run: uv run flake8 src tests

The same job is the natural home for the other two layers (uv run ruff check, uv run mypy). A team that has not adopted the dependency yet can still audit every PR with one line and zero repo changes:

      - run: uvx --with anti-slop-py flake8 --enable-extensions=ASD,ASF src tests

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

anti_slop_py-0.1.1.tar.gz (22.7 kB view details)

Uploaded Source

Built Distribution

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

anti_slop_py-0.1.1-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

File details

Details for the file anti_slop_py-0.1.1.tar.gz.

File metadata

  • Download URL: anti_slop_py-0.1.1.tar.gz
  • Upload date:
  • Size: 22.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for anti_slop_py-0.1.1.tar.gz
Algorithm Hash digest
SHA256 73849f0d2c6ad47edc46e4d009e0f410da7c83add60c9e427724822deff9840f
MD5 76b6c69f1d7127b9a7deb72c05149086
BLAKE2b-256 e001bc12a485ff673a1d09eac46e6d02b553504e1e3fb0c1add448dc62e09033

See more details on using hashes here.

Provenance

The following attestation bundles were made for anti_slop_py-0.1.1.tar.gz:

Publisher: publish.yml on infoslack/anti-slop-py

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

File details

Details for the file anti_slop_py-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: anti_slop_py-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 29.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for anti_slop_py-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 727bb26404abfa1273693c852c839cb94d4277e1fc41c943923169fe06b2342c
MD5 0dffe494c4d0b44171e3adca61bf5747
BLAKE2b-256 07f99c6b1be840aa368fb1defcc5db4b8fa12f00925dc9b14f9782ba682cad15

See more details on using hashes here.

Provenance

The following attestation bundles were made for anti_slop_py-0.1.1-py3-none-any.whl:

Publisher: publish.yml on infoslack/anti-slop-py

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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page