argot docs GitHub ↗

What it catches

Three real examples — all valid, typed, lint-clean — that every other tool stays silent on.

argot catches code that is technically fine but socially wrong for this project. The four shapes it sees most often:

SignalWhat it means
LLM paste-throughA block whose style diverges sharply from the surrounding file
Convention driftError handling, logging, or patterns that don’t match the repo
Foreign paradigmClass-based OOP in a functional codebase, the wrong import style
Stylistic outlierCorrect code that doesn’t sound like anyone on this team wrote it

The examples below are pulled from argot’s FastAPI benchmark catalog. All three are syntactically valid, fully typed, lint-clean, and pass mypy strict. Every other tool in CI is silent on them.

1. Wrong exception type

# flagged — ValueError instead of HTTPException
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db=Depends(get_db)) -> UserResponse:
    user = db.get(user_id)
    if user is None:
        raise ValueError(f"User {user_id} not found")  # propagates as 500, not 404
    return user

Decorators, Depends, response_model, the typed return — all idiomatic FastAPI. The break is one token sequence: a bare ValueError instead of HTTPException(status_code=...). The type checker is happy. The linter has nothing. argot catches it because the FastAPI corpus’s exception vocabulary is HTTPException, not Python’s built-ins.

2. Manual status check vs raise_for_status()

# flagged — structural shape, not vocabulary
@router.get("/users/{user_id}")
async def proxy_get_user(user_id: int) -> dict[str, Any]:
    response = _http_client.get(f"/v1/users/{user_id}")
    if response.status_code >= 400:
        raise HTTPException(status_code=response.status_code, detail=response.text)
    return response.json()

Every individual token here exists in the corpusresponse.status_code, HTTPException, response.json(). What’s missing is the branching shape: this corpus uses response.raise_for_status() to propagate downstream errors, not a manual if status_code >= 400. No linter can encode that preference; argot picks it up because the distribution over short token windows captures the structural difference.

3. Sync blocking in an async codebase

# flagged — sync def + blocking I/O on a hot path
@router.get("/users")
def list_users() -> list[dict[str, Any]]:
    response = httpx.get(f"{UPSTREAM_URL}/v1/users")  # blocks the worker thread
    return response.json()

@router.get, the path, the return type — all idiomatic. The break is def instead of async def, paired with the sync httpx.get(...) instead of await client.get(...). The type checker is happy. argot picks it up because sync endpoints with blocking I/O are structurally absent from the corpus — which is built around async def + await.


The thread through all three: the tokens are familiar; the combination isn’t. That’s the gap between “valid” and “ours,” and it’s the gap argot is built to close.