CLAUDE.md Python Claude Code FastAPI Django uv ruff pytest templates 2026

CLAUDE.md for Python Projects: Complete Template + Rules (2026)

The Prompt Shelf ·

Python’s AI coding landscape shifted significantly in 2025-2026. The old requirements.txt + pip + flake8 + black stack has been largely replaced by uv for dependency management, ruff for linting and formatting, and pyproject.toml as the single configuration source. If your CLAUDE.md was written a year ago, it may be guiding Claude toward the old stack.

This guide provides complete, production-ready CLAUDE.md templates for Python projects in 2026, with variants for FastAPI, Django, and data science workloads.

Why Python Needs a Different CLAUDE.md

Python has more ecosystem fragmentation than most languages. The same project can use any of three dependency managers (pip, poetry, uv), two formatter/linter paradigms (pre-ruff stack vs. ruff), three major test frameworks (pytest, unittest, nose), and wildly different typing strictness levels.

Claude is trained on all of this. Without explicit guidance, it will pick a combination — probably not yours. Common issues:

  • Using pip install in a repo that uses uv sync
  • Importing typing.Optional instead of X | None (Python 3.10+ syntax)
  • Writing List[str] instead of list[str] (pre-3.9 style)
  • Using Dict[str, Any] from typing instead of dict[str, Any]
  • Creating requirements.txt updates instead of editing pyproject.toml
  • Using print() for debug output instead of the project’s logger
  • Writing unittest-style tests in a pytest project

A well-configured CLAUDE.md eliminates most of these before they happen.

Universal Python Base Template

This base applies to almost any Python project. Extend it with the framework-specific sections below.

# Project

[One sentence description. Stack, purpose, nothing more.]

## Python Version

Python 3.12 (minimum 3.11). Use modern syntax:
- `X | None` instead of `Optional[X]`
- `list[str]` instead of `List[str]`
- `dict[str, Any]` instead of `Dict[str, Any]`
- `tuple[int, str]` instead of `Tuple[int, str]`
- `type X = ...` for type aliases (Python 3.12)
- Match statements for enum/type dispatch where appropriate

## Commands

```bash
# Install / sync
uv sync                  # install all deps including dev
uv sync --no-dev         # production deps only
uv add <package>         # add a dependency
uv add --dev <package>   # add a dev dependency

# Run
uv run python -m app     # or however the project is invoked
uv run fastapi dev       # for FastAPI projects

# Tests
uv run pytest                         # all tests
uv run pytest tests/unit/             # unit tests only
uv run pytest -k "test_auth"          # filter by name
uv run pytest -x                      # stop on first failure
uv run pytest --cov=src --cov-report=term-missing  # with coverage

# Lint and format
uv run ruff check .      # lint
uv run ruff check --fix  # lint with autofix
uv run ruff format .     # format

# Type check
uv run mypy src/

Project Structure

src/
  [package_name]/     # main package
tests/
  unit/               # fast, no I/O
  integration/        # DB and external services
pyproject.toml        # single source of config
.python-version       # pin Python version for uv

Off-Limits

  • pyproject.toml — do not change [build-system], [tool.ruff], or [tool.mypy] sections
  • uv.lock — do not edit manually; run uv sync after any change to deps
  • Never add requirements.txt — this project uses uv exclusively

Code Style

Enforced by ruff. Key rules:

  • Line length: 88 (Black-compatible)
  • Import order: stdlib → third-party → local, separated by blank lines
  • No unused imports — ruff will catch them
  • f-strings over .format() and % formatting
  • Walrus operator (:=) for pattern-match assignments only

Type annotations required for:

  • All function signatures (parameters + return type)
  • Class attributes
  • Module-level variables that are not obvious primitives

Anti-Patterns

  • No bare except: — always except SpecificError: or except (Error1, Error2):
  • No from module import *
  • No print() for debug output — use logging.getLogger(__name__)
  • No hardcoded credentials or connection strings — use environment variables
  • No mutable default arguments: def f(x: list = []) is wrong
  • No type: ignore comments without an explanation of why
  • No Any unless genuinely necessary (document why)

Testing Conventions

pytest with fixtures, no test classes unless inheritance is genuinely needed.

Test files: tests/unit/test_<module>.py, tests/integration/test_<feature>.py

# Good test structure
def test_function_does_thing_when_condition(
    some_fixture: SomeType,
) -> None:
    # arrange
    ...
    # act
    result = function_under_test(some_fixture)
    # assert
    assert result == expected

# Parametrize for multiple cases
@pytest.mark.parametrize("input,expected", [
    ("case1", result1),
    ("case2", result2),
])
def test_function_handles_cases(input: str, expected: str) -> None:
    assert function(input) == expected

For async tests: @pytest.mark.asyncio from anyio (not asyncio directly).

Before Marking Done

  • uv run ruff check . passes
  • uv run ruff format --check . passes
  • uv run mypy src/ passes with no new errors
  • uv run pytest passes
  • All new functions/methods have type annotations
  • New public functions have docstrings

## FastAPI-Specific Template

```markdown
# Project

FastAPI REST API serving [brief description].

## Commands

```bash
uv run fastapi dev src/app/main.py    # dev with hot reload
uv run fastapi run src/app/main.py    # production mode
uv run pytest tests/ -v               # all tests including API

Project Structure

src/
  app/
    main.py              # FastAPI app instance, router registration
    routers/             # one file per domain (auth.py, users.py)
    models/              # SQLAlchemy ORM models
    schemas/             # Pydantic request/response schemas
    dependencies/        # FastAPI dependency injection
    services/            # business logic (no HTTP concerns here)
    repositories/        # data access layer
    core/
      config.py          # pydantic-settings BaseSettings
      database.py        # engine, session factory
      security.py        # auth utilities
tests/
  conftest.py            # shared fixtures
  unit/                  # service/repo layer, no HTTP
  integration/           # httpx TestClient tests against DB

Dependency Injection Pattern

Use FastAPI’s dependency system. Never access db, config, or current_user directly in router functions.

# Good
@router.get("/users/{user_id}")
async def get_user(
    user_id: int,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
    user_service: UserService = Depends(get_user_service),
) -> UserResponse:
    return await user_service.get_user(db, user_id, current_user)

# Never
@router.get("/users/{user_id}")
async def get_user(user_id: int) -> UserResponse:
    db = next(get_db())  # wrong
    ...

Response Models

Always use Pydantic response models. Never return ORM objects directly.

# Schema for response
class UserResponse(BaseModel):
    id: int
    email: str
    created_at: datetime
    
    model_config = ConfigDict(from_attributes=True)

Error Handling

Use HTTPException for HTTP errors. Use custom exceptions for business logic.

# Business logic errors
class UserNotFoundError(ValueError):
    pass

# In service
def get_user(user_id: int) -> User:
    user = db.query(User).get(user_id)
    if not user:
        raise UserNotFoundError(f"User {user_id} not found")

# In router
try:
    user = user_service.get_user(user_id)
except UserNotFoundError:
    raise HTTPException(status_code=404, detail="User not found")

Testing FastAPI Endpoints

Use httpx.AsyncClient with ASGITransport. Never use TestClient for new tests.

@pytest.fixture
async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test"
    ) as client:
        yield client

async def test_get_user_returns_200(client: AsyncClient, test_user: User) -> None:
    response = await client.get(f"/api/v1/users/{test_user.id}")
    assert response.status_code == 200
    data = response.json()
    assert data["email"] == test_user.email

Anti-Patterns (FastAPI-Specific)

  • No response_model=None — always specify response model
  • No sync route handlers for DB operations — use async def
  • No db.commit() inside route handlers — use service layer
  • No business logic in routers — routes orchestrate, services compute
  • No session.execute(text("raw SQL")) — use ORM

## Django-Specific Template

```markdown
# Project

Django application for [brief description]. Django 5.x, Python 3.12.

## Commands

```bash
# Run
uv run python manage.py runserver

# Database
uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py showmigrations

# Tests
uv run pytest                                    # all tests
uv run pytest apps/users/tests/                  # specific app
uv run pytest --reuse-db                         # reuse test DB (faster)

# Management
uv run python manage.py shell_plus               # enhanced shell
uv run python manage.py createsuperuser

Project Structure

config/
  settings/
    base.py          # shared settings
    local.py         # local dev overrides
    production.py    # production settings
apps/
  users/             # one app per domain
    models.py
    views.py
    urls.py
    serializers.py   # DRF serializers
    tests/
      test_models.py
      test_views.py
manage.py
pyproject.toml

Django REST Framework Patterns

Viewsets for CRUD resources, APIView for non-CRUD endpoints.

# Good: ViewSet for standard CRUD
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self) -> QuerySet:
        return super().get_queryset().filter(
            organization=self.request.user.organization
        )

# Never: accessing request.user outside views

Database Rules

  • All database access through the ORM. No raw SQL except for complex aggregations that cannot be expressed otherwise (use RawSQL sparingly, document why).
  • Always use select_related() and prefetch_related() to avoid N+1.
  • Migrations: run makemigrations after every model change. Never edit migration files manually.
  • Never call save() in a loop — use bulk_create() or bulk_update().

Testing Django

Use pytest-django fixtures. No unittest.TestCase for new tests.

@pytest.mark.django_db
def test_user_creation(user_factory: UserFactory) -> None:
    user = user_factory.create(email="[email protected]")
    assert User.objects.filter(email="[email protected]").exists()

Anti-Patterns (Django-Specific)

  • No business logic in views.py — use service functions in services.py
  • No model methods that access other models — keep models data-only
  • No django.conf.settings imported directly in app code — use a settings module
  • No User.objects.all() in views — always filter to the current user’s scope
  • No signals for business logic — explicit service calls only

## Data Science Template

```markdown
# Project

[Brief description of the ML/data pipeline project.]
Target audience: data scientists and ML engineers.

## Python Version

Python 3.12. NumPy 2.x syntax where applicable.

## Commands

```bash
# Environment
uv sync                         # sync all deps

# Jupyter
uv run jupyter lab              # start notebook server

# Tests
uv run pytest tests/            # unit tests for utilities
uv run pytest tests/ --slow     # include slow integration tests

# Data pipeline
uv run python -m pipeline.run   # run full pipeline
uv run python -m pipeline.run --stage=preprocess  # single stage

# Model
uv run python -m train --config=configs/experiment.yaml
uv run python -m evaluate --run-id=<mlflow_run_id>

Computation Rules

  • NumPy operations over Python loops for array computation
  • No in-place modification of NumPy arrays unless performance-critical (document why)
  • Pandas: use pd.DataFrame.pipe() for transformation chains; avoid chained indexing
  • Type annotations for numerical code: npt.NDArray[np.float64], not np.ndarray
  • Random state: always pass random_state=42 or use a seeded RNG — never unseeded

Configuration

Use OmegaConf or Hydra for experiment config, not hardcoded values.

# Good
@dataclass
class TrainingConfig:
    learning_rate: float = 1e-3
    batch_size: int = 32
    epochs: int = 100

# Never
LEARNING_RATE = 0.001  # global constant

Experiment Tracking

MLflow is configured. Every experiment run must:

  • Log hyperparameters with mlflow.log_params()
  • Log metrics per epoch with mlflow.log_metric()
  • Save model artifact with mlflow.log_artifact()

No untracked experiments — if it is worth running, it is worth tracking.

Data Handling

  • Raw data in data/raw/ — never modified after download
  • Processed data in data/processed/ — regeneratable from raw
  • Never commit data files — they are in .gitignore
  • Large files go to the configured object storage, not the repo

Anti-Patterns (Data Science-Specific)

  • No hardcoded file paths — use pathlib.Path relative to project root
  • No df.iterrows() — vectorize or use .apply() sparingly
  • No train/test data leakage — fit preprocessing on train, transform test
  • No model artifacts committed to git — use MLflow artifact store
  • No notebook-only logic — extract reusable functions to src/ modules

## Making CLAUDE.md Maintainable

Python projects evolve fast. The places where CLAUDE.md most often becomes wrong:

**Dependency manager changes.** If the project switches from Poetry to uv, every command in CLAUDE.md needs updating. Add a note in the relevant PR to update CLAUDE.md.

**Python version bumps.** When the minimum Python version increases, the type syntax recommendations can change. Python 3.12's `type X = Y` syntax for type aliases was not available in 3.9.

**Framework major versions.** Django 5.x, FastAPI 0.115+, and Pydantic v2 all changed APIs. If your CLAUDE.md was written for older versions, Claude will generate deprecated patterns.

**Test framework adoption.** Many projects are mid-migration from `unittest` to `pytest`. If you are in that state, CLAUDE.md should say: "New tests use pytest, not unittest. If you see unittest-style tests, that is legacy code — do not add more."

A simple process: when you review a PR that has Claude-generated Python code with outdated patterns, update CLAUDE.md in that same PR.

## Related Resources

For CLAUDE.md anti-patterns and what makes rules effective, see [CLAUDE.md best practices](/blog/claude-md-best-practices-2026). For a full library of templates by project type, see [10 CLAUDE.md templates by use case](/blog/claude-md-templates-by-use-case). For how CLAUDE.md compares to AGENTS.md for cross-tool environments, see the [format comparison guide](/blog/claude-md-conventions-md-agents-md-comparison-2026).

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules