Files
AI/preferences/python-web.md

4.1 KiB

Python Web Stack Standards

Stack

  • API: FastAPI (async)
  • ORM: async SQLAlchemy 2.0 with ORM models (not raw queries)
  • Database: MySQL 8
  • Runtime: Docker Compose
  • Reverse proxy: Nginx
  • Container management: Portainer

Project Layout

project-name/
├── docker-compose.yml
├── .env.example          # Template — always include this, never commit .env
├── .gitignore
├── README.md
├── nginx/
│   └── default.conf
└── backend/
    ├── Dockerfile
    ├── requirements.txt
    └── app/
        ├── main.py           # App entry point, router registration, startup events
        ├── config.py         # Settings loaded from environment via pydantic-settings
        ├── database.py       # Async SQLAlchemy engine and session factory
        ├── dependencies.py   # Shared FastAPI dependencies (auth, db session, etc.)
        ├── models/           # SQLAlchemy ORM table definitions
        ├── schemas/          # Pydantic request/response models
        ├── routers/          # API endpoint handlers, one file per resource
        └── utils/            # Shared helpers

Configuration & Secrets

  • All secrets and environment-specific values go in .env — never hardcode them
  • Always provide .env.example with placeholder values and comments explaining each variable
  • Load settings via pydantic-settings:
# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    db_host: str
    db_port: int = 3306
    db_name: str
    db_user: str
    db_password: str
    secret_key: str

    class Config:
        env_file = ".env"

settings = Settings()

Database (SQLAlchemy 2.0 Async)

  • Use async engine and session factory
  • Define models with DeclarativeBase
  • One model per file in models/
# app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase

engine = create_async_engine(DATABASE_URL, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

class Base(DeclarativeBase):
    pass
  • Inject DB sessions via FastAPI dependencies (not global sessions)
  • Use async with session.begin() for transactions

Models

  • One file per table in models/
  • Use typed columns with SQLAlchemy 2.0 Mapped syntax:
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(unique=True)
    email: Mapped[str]

Schemas (Pydantic)

  • Separate schemas for Create, Update, and Response — don't reuse the same schema for all three
  • Response schemas should never expose password hashes or internal fields
class UserCreate(BaseModel): ...
class UserResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)

Routers

  • One router file per resource (e.g., routers/users.py, routers/auth.py)
  • Register all routers in main.py with a consistent prefix and tags

Docker Compose

  • Use named volumes for persistent data (database, uploads)
  • Always define a healthcheck for the database service
  • Use depends_on with condition: service_healthy for the backend
  • Keep the Nginx config in a nginx/ folder and mount it as a volume
services:
  db:
    image: mysql:8
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    depends_on:
      db:
        condition: service_healthy

Testing

  • Use pytest with pytest-asyncio for async tests
  • Always provide a test for each router endpoint at minimum (happy path + 1 failure case)
  • Use a separate test database — never run tests against the real DB
  • Run with: pytest -v

Comments

  • Docstrings on all functions that aren't self-explanatory
  • Comment the why for non-obvious logic, not the what