# How to lint and type-check a FastAPI project


Configure [Ruff](https://pydevtools.com/handbook/reference/ruff.md) to catch blocking calls in async endpoints and [ty](https://pydevtools.com/handbook/reference/ty.md) to check annotated dependencies and Pydantic models.

## Prerequisites

Start in an existing FastAPI project with Pydantic models and [uv](https://pydevtools.com/handbook/reference/uv.md). For project creation, follow [Set up a FastAPI project with uv](https://pydevtools.com/handbook/tutorial/set-up-a-fastapi-project-with-uv.md).

## Install the checkers

```bash
uv add --dev ruff ty
```

uv lists Ruff and ty among the installed packages and records them in `pyproject.toml`. The `--dev` flag marks both tools as {{< term "dev-dependency" "development dependencies" >}}, allowing production installs to exclude them with `uv sync --no-dev`. When starting the application through `uv run`, also pass `--no-dev` to avoid reinstalling them.

## Enable rules for FastAPI and async code

Merge these selections into the existing `[tool.ruff.lint]` table in [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md), or add the table if absent:

```toml
[tool.ruff.lint]
extend-select = [
    "FAST",      # FastAPI signatures and redundant response models
    "ASYNC210",  # Blocking HTTP calls in async functions
    "ASYNC212",  # Blocking HTTPX calls in async functions
    "ASYNC230",  # Blocking file opens in async functions
    "ASYNC251",  # time.sleep in async functions
    "B",         # Bugbear, including calls in argument defaults
    "I",         # Import sorting
]
```

These async rules catch known blocking APIs; they do not detect every blocking operation. Replace `time.sleep(...)` inside `async def` with `await asyncio.sleep(...)`, and use async clients for network calls.

## Annotate dependencies and model fields

Use `Annotated[Filters, Depends(get_filters)]` for injected parameters. FastAPI uses the metadata to call the provider; ty uses `Filters` to check attribute access and return types. This pattern also avoids B008 warnings about `Depends(...)` in argument defaults, so keep B008 enabled.

Adapt these annotations to your models and endpoints. This standalone example creates its own `app`; keep your existing instance when integrating it:

```python
from typing import Annotated

from fastapi import Depends, FastAPI
from pydantic import BaseModel, Field

app = FastAPI()


class Filters(BaseModel):
    limit: int = Field(default=20, ge=1)
    tags: list[str] = Field(default_factory=list)


def get_filters() -> Filters:
    return Filters()


@app.get("/limit")
async def read_limit(filters: Annotated[Filters, Depends(get_filters)]) -> int:
    return filters.limit
```

Keep field defaults and factories in assignment-form `Field(...)` so constructor parameters remain visible to type checkers. ty checks these Pydantic fields without a plugin: `Filters(limit="20")` passes checking, while `Filters(limit=[])` produces `invalid-argument-type`. Keep Pydantic's runtime validation for incoming data.

Match each provider's return annotation to the injected parameter's type. A provider annotated `-> str` still passes ty when wired into `Annotated[Filters, Depends(...)]`; checking succeeds even though the endpoint expects a `Filters` instance. Exercise that wiring through a request before relying on it.

## Run the project checks

`uv run` executes the command inside the project's virtual environment, ensuring all dependencies are installed first. No FastAPI-specific ty configuration is required for this project layout.

```bash
uv run ruff check .
uv run ruff format --check .
uv run ty check
```

On clean code, Ruff's linter and ty each print `All checks passed!`; the formatter reports how many files are already formatted. Otherwise, fix the reported locations and rerun the checks. For a first type-checking workflow, follow [Set up type checking with ty](https://pydevtools.com/handbook/tutorial/set-up-type-checking-with-ty.md).
