guide9 min read8d ago

Best Cursor Rules for Python & FastAPI (2026)

10 production-ready .cursorrules configurations for Python and FastAPI projects. Type hints, async patterns, Pydantic models, testing — all copy-paste ready.

Best Cursor Rules for Python & FastAPI (2026)
cursorcursor rulescursorrulespythonfastapiAI codingdeveloper tools2026

Best Cursor Rules for Python & FastAPI (2026)

By David Henderson | March 26, 2026 | 13 min read


TL;DR: Cursor generates Python code that works but often ignores your project's conventions — missing type hints, wrong async patterns, no Pydantic validation, tests that use unittest instead of pytest. A .cursorrules file fixes all of this. Here are 10 battle-tested configurations for Python and FastAPI projects that I use on every project.

Table of Contents

  1. The Python-Specific Problem
  2. The 10 Rules
  3. Putting It All Together
  4. Frequently Asked Questions

The Python-Specific Problem {#python-problem}

Python is flexible. That is its greatest strength and its biggest problem when working with AI code generation.

Without rules, Cursor will generate Python code that runs — but it will not match your project. I have seen it do all of the following in a single afternoon:

  • Generate synchronous route handlers in an async FastAPI project
  • Use dict instead of Pydantic models for request/response validation
  • Write functions without type hints in a fully typed codebase
  • Import os.environ directly instead of using your settings module
  • Create tests with unittest.TestCase when your project uses pytest
  • Use requests instead of httpx for async HTTP calls
  • Put business logic in route handlers instead of service layers

These are not bugs. They are valid Python. They are just not your Python.

Every rule below addresses a real pattern I have corrected repeatedly. If you are building Python APIs with FastAPI, these rules will save you hours of cleanup per week.

For more Cursor rules across every language and framework, browse the Skiln Cursor directory.


The 10 Rules {#the-10-rules}

Rule 1: Python Foundation

The baseline rule that applies to every Python project.

# Python Project Standards

This is a Python 3.12+ project. Follow these rules for ALL Python code:

## Type Hints
- ALWAYS use type hints for function parameters and return types
- Use `str | None` syntax (not Optional[str]) — we're on Python 3.12+
- Use built-in generics: `list[str]`, `dict[str, int]` — not `List`, `Dict` from typing
- Complex types go in a `types.py` file

## Imports
- Standard library first, third-party second, local third (isort order)
- Use absolute imports: `from app.services.user import UserService` — not relative imports
- Never use wildcard imports: `from module import *`

## Style
- Follow PEP 8
- Use snake_case for functions and variables
- Use PascalCase for classes
- Use UPPER_SNAKE_CASE for constants
- Docstrings use Google style (not reStructuredText)

## Example
from datetime import datetime

from fastapi import HTTPException

from app.models.user import User
from app.services.auth import AuthService


def get_user_display_name(user: User, include_title: bool = False) -> str:
    """Build a display name for the user.

    Args:
        user: The user object.
        include_title: Whether to prepend the user's title.

    Returns:
        The formatted display name.
    """
    if include_title and user.title:
        return f"{user.title} {user.first_name} {user.last_name}"
    return f"{user.first_name} {user.last_name}"

Rule 2: FastAPI Application Structure

Establishes the project layout and routing conventions.

# FastAPI Application Structure

This project uses FastAPI with the following structure:

app/
  main.py          — FastAPI app instance and startup
  api/
    v1/
      routes/      — Route handlers (thin — delegate to services)
      dependencies/ — Dependency injection functions
  models/          — SQLAlchemy/SQLModel database models
  schemas/         — Pydantic request/response schemas
  services/        — Business logic (all logic lives here)
  repositories/    — Database access layer
  core/
    config.py      — Settings via pydantic-settings
    security.py    — Auth utilities
    exceptions.py  — Custom exception classes

## Rules
- Route handlers are THIN. They validate input, call a service, return output.
- Business logic lives in services/ — NEVER in route handlers.
- Database queries live in repositories/ — NEVER in services directly.
- Each layer only talks to the layer below it: routes → services → repositories.

Rule 3: FastAPI Route Handlers

Controls how Cursor generates API endpoints.

# FastAPI Route Handlers

## Rules
- ALL route handlers must be async: `async def` not `def`
- Use dependency injection for common needs (db session, current user, pagination)
- Use Pydantic models for request bodies and responses — NEVER use raw dicts
- Always set `response_model` on the decorator
- Use proper HTTP status codes: 201 for creation, 204 for deletion
- Use HTTPException for errors — never return error dicts with 200 status

## Example
from fastapi import APIRouter, Depends, HTTPException, status

from app.api.v1.dependencies.auth import get_current_user
from app.api.v1.dependencies.database import get_db
from app.schemas.project import ProjectCreate, ProjectResponse
from app.services.project import ProjectService

router = APIRouter(prefix="/projects", tags=["projects"])


@router.post("/", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
async def create_project(
    data: ProjectCreate,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> ProjectResponse:
    """Create a new project."""
    service = ProjectService(db)
    project = await service.create(data, owner_id=current_user.id)
    return project


@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project(
    project_id: int,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> ProjectResponse:
    """Get a project by ID."""
    service = ProjectService(db)
    project = await service.get_by_id(project_id)
    if not project:
        raise HTTPException(status_code=404, detail="Project not found")
    return project

Rule 4: Pydantic Schemas

For request/response validation with Pydantic v2.

# Pydantic Schemas

This project uses Pydantic v2 for ALL data validation.

## Rules
- NEVER use raw dicts for API input/output — always use Pydantic models
- Separate schemas: {Model}Create (input), {Model}Response (output), {Model}Update (partial)
- Use `model_config = ConfigDict(from_attributes=True)` for ORM compatibility
- Use Field() for validation constraints and documentation
- Use custom validators sparingly — prefer Field constraints when possible

## Example
from pydantic import BaseModel, ConfigDict, Field, EmailStr


class UserCreate(BaseModel):
    email: EmailStr
    name: str = Field(min_length=1, max_length=100)
    bio: str | None = Field(default=None, max_length=500)


class UserResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    email: str
    name: str
    bio: str | None
    created_at: datetime


class UserUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=100)
    bio: str | None = Field(default=None, max_length=500)

Rule 5: Async Database Access

For projects using SQLAlchemy 2.0 with async.

# Database Access

This project uses SQLAlchemy 2.0 with async PostgreSQL.

## Rules
- ALWAYS use async sessions: AsyncSession, not Session
- Use the repository pattern — database queries live in repositories/, not services/
- NEVER use synchronous SQLAlchemy APIs
- Use `select()` statements — not the legacy `session.query()` API
- Always use `async with` for session management

## Example
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.user import User


class UserRepository:
    def __init__(self, db: AsyncSession) -> None:
        self.db = db

    async def get_by_id(self, user_id: int) -> User | None:
        result = await self.db.execute(select(User).where(User.id == user_id))
        return result.scalar_one_or_none()

    async def get_by_email(self, email: str) -> User | None:
        result = await self.db.execute(select(User).where(User.email == email))
        return result.scalar_one_or_none()

    async def create(self, user: User) -> User:
        self.db.add(user)
        await self.db.commit()
        await self.db.refresh(user)
        return user

Rule 6: Configuration Management

Prevents hardcoded secrets and inconsistent config access.

# Configuration

Use pydantic-settings for ALL configuration. NEVER use os.environ directly.

## Setup
# app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
    )

    # Database
    database_url: str
    database_echo: bool = False

    # Auth
    secret_key: str
    access_token_expire_minutes: int = 30

    # External APIs
    stripe_secret_key: str
    stripe_webhook_secret: str

    # App
    app_name: str = "MyApp"
    debug: bool = False


settings = Settings()

## Usage
from app.core.config import settings

# Correct:
db_url = settings.database_url

# WRONG — never do this:
import os
db_url = os.environ["DATABASE_URL"]

Rule 7: Error Handling

Standardizes error patterns across the project.

# Error Handling

## Custom Exceptions
Define custom exceptions in app/core/exceptions.py. Map them to HTTP responses.

# app/core/exceptions.py
class AppError(Exception):
    """Base exception for application errors."""
    def __init__(self, message: str, code: str = "UNKNOWN") -> None:
        self.message = message
        self.code = code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: int | str) -> None:
        super().__init__(f"{resource} with id {id} not found", code="NOT_FOUND")

class PermissionDeniedError(AppError):
    def __init__(self, action: str = "perform this action") -> None:
        super().__init__(f"You do not have permission to {action}", code="FORBIDDEN")

## Exception Handlers (register in main.py)
@app.exception_handler(NotFoundError)
async def not_found_handler(request: Request, exc: NotFoundError) -> JSONResponse:
    return JSONResponse(status_code=404, content={"error": exc.message, "code": exc.code})

## Rules
- Raise custom exceptions in services — NEVER raise HTTPException in services
- Convert custom exceptions to HTTP responses in exception handlers
- Log all 500-level errors with full context
- Return structured error responses: {"error": "message", "code": "ERROR_CODE"}

Rule 8: Testing with Pytest

For projects using pytest with async support.

# Testing

This project uses pytest + pytest-asyncio + httpx for testing.

## Rules
- ALL test files named `test_{module}.py`
- ALL test functions named `test_{behavior_being_tested}`
- Use pytest fixtures — NEVER use setUp/tearDown or unittest.TestCase
- Use httpx.AsyncClient for API tests — NOT requests or TestClient
- Mock external services — never hit real APIs in tests
- Use factory functions for test data — not fixtures with hardcoded values

## Example
import pytest
from httpx import AsyncClient

from app.main import app
from tests.factories import create_test_user


@pytest.fixture
async def client():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        yield ac


@pytest.fixture
async def authenticated_client(client: AsyncClient):
    user = await create_test_user()
    token = create_access_token(user.id)
    client.headers["Authorization"] = f"Bearer {token}"
    return client


async def test_create_project_returns_201(authenticated_client: AsyncClient):
    response = await authenticated_client.post("/api/v1/projects/", json={
        "name": "Test Project",
        "description": "A test project",
    })
    assert response.status_code == 201
    assert response.json()["name"] == "Test Project"


async def test_create_project_without_auth_returns_401(client: AsyncClient):
    response = await client.post("/api/v1/projects/", json={"name": "Test"})
    assert response.status_code == 401

Rule 9: Logging

Ensures consistent, structured logging.

# Logging

Use structlog for structured logging. NEVER use print() for anything that should be logged.

## Setup
import structlog

logger = structlog.get_logger()

## Rules
- Use appropriate log levels: debug, info, warning, error, critical
- Always include context: logger.info("user_created", user_id=user.id, email=user.email)
- NEVER log sensitive data: passwords, tokens, API keys, credit card numbers
- Log at service boundaries: incoming requests, outgoing API calls, database errors
- Use print() ONLY for CLI scripts and local debugging — never in application code

## Example
from app.core.logging import logger

class UserService:
    async def create(self, data: UserCreate) -> User:
        logger.info("creating_user", email=data.email)
        try:
            user = await self.repo.create(User(**data.model_dump()))
            logger.info("user_created", user_id=user.id)
            return user
        except IntegrityError:
            logger.warning("duplicate_email", email=data.email)
            raise DuplicateEmailError(data.email)

Rule 10: Dependency Injection

Controls how FastAPI dependencies are structured.

# Dependency Injection

Use FastAPI's Depends() system for all shared dependencies.

## Common Dependencies
# app/api/v1/dependencies/database.py
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_factory() as session:
        yield session

# app/api/v1/dependencies/auth.py
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(
    token: HTTPAuthorizationCredentials = Depends(security),
    db: AsyncSession = Depends(get_db),
) -> User:
    user = await verify_token_and_get_user(token.credentials, db)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

## Rules
- Dependencies go in app/api/v1/dependencies/
- Each dependency is a function (not a class) that uses Depends() for its own dependencies
- NEVER instantiate services or repositories directly in route handlers — use dependency injection
- Use `yield` dependencies for resources that need cleanup (db sessions, file handles)

Putting It All Together {#putting-it-together}

For a typical FastAPI project, I combine these rules into a single .cursorrules file. My recommended combination:

  • Rule 1 (Python Foundation) — always
  • Rule 2 (Application Structure) — always for FastAPI
  • Rule 3 (Route Handlers) — always for FastAPI
  • Rule 4 (Pydantic Schemas) — always for FastAPI
  • Rule 5 (Async Database) — if using SQLAlchemy async
  • Rule 6 (Configuration) — always
  • Rule 8 (Testing) — if writing tests

That gives you roughly 1,200 words of rules — well within Cursor's context limits while covering the patterns that matter most.

The key is specificity. "Use FastAPI best practices" tells Cursor nothing. "Route handlers are async, use Pydantic for validation, delegate to services" tells it exactly what to generate.

For more rules across Python, Go, Rust, and every other major language, check the Skiln Cursor directory — we maintain the largest searchable collection of community-contributed Cursor configurations.


Frequently Asked Questions {#faq}

Do .cursorrules work for Django projects too?

The Python-specific rules (Rule 1: type hints, imports, style) work for any Python project. The FastAPI-specific rules (Rules 2-5, 10) would need to be adapted for Django's patterns — class-based views, Django REST Framework serializers, Django ORM instead of SQLAlchemy. The structure is the same, just different examples.

Can I use .cursorrules with Poetry or uv projects?

Yes. The .cursorrules file is independent of your package manager. Whether you use pip, Poetry, uv, or conda, the rules file works the same way. Cursor reads it from your project root regardless of how dependencies are managed.

How do I handle different rules for different Python versions?

Specify the Python version in Rule 1. If you are on Python 3.9, change the type hint syntax to Optional[str] and List[str] instead of the 3.10+ union syntax. The rules are just text — adjust the examples to match your version.

Should I commit .cursorrules to version control?

Yes. Commit it to your repository so every team member gets the same AI-generated code quality. The file contains no secrets — it is just instructions for how code should be written. Treat it like .editorconfig or .eslintrc.

Do these rules work with other AI tools like GitHub Copilot?

Not directly. Copilot does not read .cursorrules files. However, GitHub Copilot has its own instructions feature, and the conventions transfer. You would just format them differently. For Claude Code, you would put equivalent instructions in a CLAUDE.md file or use custom skills.


Frequently Asked Questions

Do .cursorrules work for Django projects too?
The Python-specific rules (type hints, imports, style) work for any Python project. The FastAPI-specific rules would need to be adapted for Django's patterns — class-based views, Django REST Framework serializers, Django ORM instead of SQLAlchemy.
Can I use .cursorrules with Poetry or uv projects?
Yes. The .cursorrules file is independent of your package manager. Whether you use pip, Poetry, uv, or conda, the rules file works the same way. Cursor reads it from your project root regardless of how dependencies are managed.
How do I handle different rules for different Python versions?
Specify the Python version in your foundation rule. If you are on Python 3.9, change the type hint syntax to Optional[str] and List[str] instead of the 3.10+ union syntax. The rules are just text — adjust the examples to match your version.
Should I commit .cursorrules to version control?
Yes. Commit it to your repository so every team member gets the same AI-generated code quality. The file contains no secrets — it is just instructions for how code should be written. Treat it like .editorconfig or .eslintrc.
Do these rules work with other AI tools like GitHub Copilot?
Not directly. Copilot does not read .cursorrules files. However, GitHub Copilot has its own instructions feature, and the conventions transfer. For Claude Code, you would put equivalent instructions in a CLAUDE.md file or use custom skills.

Stay in the Loop

Join 1,000+ developers. Get the best new Skills & MCPs weekly.

No spam. Unsubscribe anytime.