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 installin a repo that usesuv sync - Importing
typing.Optionalinstead ofX | None(Python 3.10+ syntax) - Writing
List[str]instead oflist[str](pre-3.9 style) - Using
Dict[str, Any]from typing instead ofdict[str, Any] - Creating
requirements.txtupdates instead of editingpyproject.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]sectionsuv.lock— do not edit manually; runuv syncafter any change to deps- Never add
requirements.txt— this project usesuvexclusively
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:— alwaysexcept SpecificError:orexcept (Error1, Error2): - No
from module import * - No
print()for debug output — uselogging.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
Anyunless 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 pytestpasses - 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
RawSQLsparingly, document why). - Always use
select_related()andprefetch_related()to avoid N+1. - Migrations: run
makemigrationsafter every model change. Never edit migration files manually. - Never call
save()in a loop — usebulk_create()orbulk_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 inservices.py - No model methods that access other models — keep models data-only
- No
django.conf.settingsimported 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], notnp.ndarray - Random state: always pass
random_state=42or 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.Pathrelative 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).