Skip to content

How to lint and type-check a FastAPI project

Configure Ruff to catch blocking calls in async endpoints and ty to check annotated dependencies and Pydantic models.

Prerequisites

Start in an existing FastAPI project with Pydantic models and uv. For project creation, follow Set up a FastAPI project with uv.

Install the checkers

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 development dependenciesA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag. , 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, or add the table if absent:

[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:

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.

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.

Last updated on