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 |
Project Setup
# 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
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
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
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
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
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
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
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
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
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 |