--- name: backend-python description: | Modern Python backend stack: FastAPI, SQLAlchemy 2.0, Pydantic v2, uv, ruff. Use when: building Python APIs, async services, or working with Python backend code. Triggers: "fastapi", "python backend", "sqlalchemy", "pydantic", "uv", "ruff", "python api", "async python". --- # Python Backend Stack > **Live docs:** Add `use context7` to prompt for up-to-date FastAPI, SQLAlchemy, Pydantic documentation. ## Tooling (2025) | Tool | Purpose | Why | |------|---------|-----| | **uv** | Package manager | 10-100x faster than pip | | **ruff** | Linter + formatter | Replaces black, isort, flake8 | | **FastAPI** | Web framework | Async, auto-docs, Pydantic | | **SQLAlchemy 2.0** | ORM | Async support, type hints | | **Pydantic v2** | Validation | 5-50x faster than v1 | | **pytest** | Testing | See [testing.md](references/testing.md) | ## Project Setup ```bash # Initialize uv init my-api cd my-api uv add fastapi sqlalchemy[asyncio] pydantic pydantic-settings uv add --dev ruff pytest pytest-asyncio httpx # pyproject.toml [tool.ruff] line-length = 100 select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "SIM"] [tool.ruff.format] quote-style = "double" [tool.pytest.ini_options] asyncio_mode = "auto" ``` ## Project Structure ``` src/ ├── main.py # FastAPI app ├── config.py # Settings (pydantic-settings) ├── db/ │ ├── __init__.py │ ├── engine.py # Async engine + session │ └── models.py # SQLAlchemy models ├── api/ │ ├── __init__.py │ ├── deps.py # Dependencies (get_db, get_user) │ └── routes/ │ ├── __init__.py │ ├── auth.py │ └── users.py ├── schemas/ # Pydantic models │ ├── __init__.py │ └── user.py └── services/ # Business logic └── user.py tests/ ├── conftest.py └── test_users.py ``` ## FastAPI Patterns ### Basic App ```python from fastapi import FastAPI from contextlib import asynccontextmanager @asynccontextmanager async def lifespan(app: FastAPI): # Startup await init_db() yield # Shutdown await close_db() app = FastAPI(lifespan=lifespan) ``` ### Route with Dependencies ```python from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter(prefix="/users", tags=["users"]) @router.get("/{user_id}", response_model=UserOut) async def get_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): user = await db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return user ``` ### Pydantic Schemas ```python from pydantic import BaseModel, EmailStr, Field from datetime import datetime class UserCreate(BaseModel): email: EmailStr name: str = Field(min_length=1, max_length=100) class UserOut(BaseModel): id: int email: str name: str created_at: datetime model_config = {"from_attributes": True} # For ORM mode ``` ### Settings ```python from pydantic_settings import BaseSettings from functools import lru_cache class Settings(BaseSettings): database_url: str secret_key: str debug: bool = False model_config = {"env_file": ".env"} @lru_cache def get_settings() -> Settings: return Settings() ``` ## SQLAlchemy 2.0 Async ### Engine Setup ```python from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker, DeclarativeBase engine = create_async_engine( "postgresql+asyncpg://user:pass@localhost/db", echo=False, pool_size=5, max_overflow=10, ) AsyncSessionLocal = sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) class Base(DeclarativeBase): pass ``` ### Models ```python from sqlalchemy import String, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from datetime import datetime class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(255), unique=True, index=True) name: Mapped[str] = mapped_column(String(100)) created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) posts: Mapped[list["Post"]] = relationship(back_populates="author") ``` ### Database Dependency ```python from typing import AsyncGenerator async def get_db() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: try: yield session await session.commit() except Exception: await session.rollback() raise ``` ### Queries ```python from sqlalchemy import select from sqlalchemy.orm import selectinload # Get one user = await db.get(User, user_id) # Query with filter stmt = select(User).where(User.email == email) result = await db.execute(stmt) user = result.scalar_one_or_none() # Eager load relationships stmt = select(User).options(selectinload(User.posts)).where(User.id == user_id) # Pagination stmt = select(User).offset(skip).limit(limit).order_by(User.created_at.desc()) result = await db.execute(stmt) users = result.scalars().all() ``` ## Error Handling ```python from fastapi import HTTPException, status from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): return JSONResponse( status_code=422, content={"error": {"code": "VALIDATION_ERROR", "details": exc.errors()}}, ) # Custom exceptions class NotFoundError(Exception): def __init__(self, resource: str, id: int): self.resource = resource self.id = id @app.exception_handler(NotFoundError) async def not_found_handler(request, exc): return JSONResponse( status_code=404, content={"error": {"message": f"{exc.resource} {exc.id} not found"}}, ) ``` ## Anti-patterns | Don't | Do Instead | |-------|------------| | `pip install` | `uv add` | | `black` + `isort` + `flake8` | `ruff` | | SQLAlchemy 1.x style | SQLAlchemy 2.0 with `Mapped[]` | | Pydantic v1 | Pydantic v2 (`model_config`, not `Config`) | | Sync database calls | Async with `asyncpg`/`aiosqlite` | | Global DB session | Dependency injection | | Business logic in routes | Services layer |