Why Python Projects Need Specific Claude Rules
Python's flexibility is both its strength and its trap. Without explicit rules, Claude will make reasonable but inconsistent choices — sync vs async, plain dicts vs dataclasses, print() vs logging, mutable defaults, missing type hints. Good CLAUDE.md rules lock in your team's conventions so Claude produces consistent, idiomatic Python every time.
This guide covers rules for general Python projects, FastAPI APIs, Django apps, and data pipelines — with copy-paste CLAUDE.md blocks for each.
General Python Rules (All Projects)
Start every Python project with these baseline rules before adding framework-specific ones:
## Python Baseline ### Version & Tooling - Python 3.11+ — use modern syntax (match statements, tomllib, Self type) - Package manager: uv (not pip or poetry) - Formatter: ruff format; Linter: ruff check (not black, flake8, or pylint) - Type checker: pyright in strict mode ### Style - Type annotations on ALL function signatures — no bare `def foo(x)` - Use `|` union syntax: `str | None` not `Optional[str]` - Prefer dataclasses or Pydantic models over plain dicts for structured data - Never use mutable default arguments (`def f(items=[])` is a bug) - One public class or a cohesive set of functions per module ### Imports - Absolute imports only — never relative imports outside of packages - Group: stdlib → third-party → local (separated by blank lines) - No wildcard imports (`from foo import *`) ### Error Handling - Never bare `except:` or `except Exception: pass` - Catch the most specific exception possible - Always log errors with context before re-raising or returning - Use custom exception classes for domain errors ### What Not to Do - Don't use `print()` for debugging — use `logging` or `structlog` - Don't shadow builtins (`list`, `dict`, `id`, `type`) - Don't put business logic in `__init__.py` files - Don't use `assert` for runtime validation (stripped in optimized mode)
FastAPI Rules
FastAPI projects benefit from rules that enforce consistent project layout, dependency injection patterns, and Pydantic v2 usage:
## FastAPI Project
### Project Layout
- app/routers/ — Route files, one per resource (users.py, items.py)
- app/services/ — Business logic, no direct DB calls
- app/repositories/ — All database access, returns domain models
- app/schemas/ — Pydantic request/response schemas
- app/models/ — SQLAlchemy ORM models
- app/deps.py — Shared FastAPI dependencies (get_db, get_current_user)
### Schemas (Pydantic v2)
- Separate schemas for Create, Update, and Read (never reuse the same model)
- Read schemas must never expose password hashes or internal fields
- Use `model_config = ConfigDict(from_attributes=True)` on ORM-backed schemas
### Routes
```python
# Always use dependency injection for db and auth
@router.get("/{user_id}", response_model=UserRead)
async def get_user(
user_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> UserRead:
user = await user_service.get_by_id(db, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return UserRead.model_validate(user)
```
### Async Rules
- All route handlers must be `async def`
- Never call sync I/O (requests, open(), time.sleep()) inside async routes
- Use `run_in_executor` only as a last resort for blocking calls
- Use `asyncio.gather()` for concurrent independent DB lookups
### Error Handling
- Use `HTTPException` with specific status codes — never return raw 500
- Raise 422 for validation errors, 401 for auth, 403 for forbidden, 404 for not found
- Add a global exception handler in `main.py` to catch and log unhandled exceptions
### Don'ts
- Don't put business logic in route handlers — delegate to services
- Don't use `response.status_code = 200` — use `status_code` param on the decorator
- Don't commit database sessions manually — use lifespan context managersDjango Rules
Django projects have strong conventions, but Claude still benefits from explicit rules about apps, ORM usage, and avoiding common pitfalls:
## Django Project
### App Structure
- One Django app per bounded domain (users, products, orders — not "utils" or "common")
- Each app: models.py, views.py, serializers.py, urls.py, admin.py, tests/
- Keep apps decoupled — apps should not import from each other's models directly
### Models
- Always define `__str__` on every model
- Use `related_name` on all ForeignKey and ManyToManyField
- Never use `null=True` on string fields — use `blank=True` and default `""`
- Add `db_index=True` on any field used in `filter()` or `order_by()` queries
- Use `select_related()` for FK lookups and `prefetch_related()` for M2M
### ORM Patterns
```python
# Use get_object_or_404 in views, not try/except DoesNotExist
user = get_object_or_404(User, pk=user_id)
# Use bulk operations for multiple rows
User.objects.bulk_create(new_users)
User.objects.filter(active=False).update(deleted_at=now())
# Never do this (N+1 query):
for order in Order.objects.all():
print(order.user.email) # hits DB for every row
# Do this instead:
for order in Order.objects.select_related("user").all():
print(order.user.email)
```
### Views & Serializers (DRF)
- Use class-based views (`APIView`, `ModelViewSet`) — not function-based views
- `serializers.ModelSerializer` for CRUD; custom `Serializer` for complex logic
- Always define `fields` explicitly — never use `fields = "__all__"`
- Use `SerializerMethodField` for computed/derived data
### Settings
- Never import from `django.conf.settings` in models — pass values as arguments
- Use `django-environ` for environment variables — never hardcode secrets
- `DEBUG = False` in production; all secrets from envData Science & Scripting Rules
For data pipelines, notebooks, and scripts, the rules are different — structure and reproducibility matter more than API design:
## Data / Scripting Projects ### Code Structure - Scripts must have a `main()` function guarded by `if __name__ == "__main__":` - Use `argparse` or `typer` for CLI arguments — never hardcode paths or params - Keep data loading, transformation, and output as separate functions ### Pandas / Polars - Prefer method chaining over intermediate variables - Never modify a DataFrame in place without a comment explaining why - Use `.copy()` when slicing to avoid SettingWithCopyWarning - For large datasets (>1M rows), use Polars or chunked Pandas reads ### Reproducibility - Pin all dependencies in requirements.txt with exact versions - Set random seeds explicitly: `np.random.seed(42)`, `random.seed(42)` - Log the shape and dtypes of DataFrames at pipeline entry points - Save intermediate outputs to disk for long-running pipelines ### Notebooks - Notebooks are for exploration only — move reusable code to .py modules - Restart kernel and run all cells before committing - No hardcoded absolute paths — use `pathlib.Path(__file__).parent`
Testing Rules
Python testing with pytest has its own set of patterns worth encoding in CLAUDE.md:
## Testing (pytest)
### Structure
- tests/ mirrors the app/ structure (tests/services/test_user_service.py)
- Prefix test files with `test_` and test functions with `test_`
- Use fixtures in conftest.py for shared setup — not setUp/tearDown
### Fixture Patterns
```python
# Use factory fixtures for test data
@pytest.fixture
def user_factory(db):
def _make(email="test@example.com", **kwargs):
return User.objects.create(email=email, **kwargs)
return _make
# Scope expensive fixtures appropriately
@pytest.fixture(scope="session")
def test_client():
with TestClient(app) as client:
yield client
```
### Rules
- Each test asserts exactly one behavior (one `assert` per test when possible)
- Use `pytest.mark.parametrize` for testing multiple inputs — never duplicate tests
- Mock at the boundary (HTTP calls, filesystem, time) — not internal functions
- Use `freezegun` for time-dependent tests — never sleep in tests
- Aim for 100% coverage on services/ and repositories/ — not on views/routesPutting It Together: A Full CLAUDE.md Template
Here's a complete CLAUDE.md for a FastAPI project that combines the sections above:
# Project: My FastAPI Service ## Stack - Python 3.12, FastAPI 0.115, SQLAlchemy 2.0 async, PostgreSQL - Pydantic v2, alembic for migrations - pytest + httpx for tests, ruff for lint/format, pyright for types - uv for dependency management ## Project Layout - app/routers/ — Route handlers (thin, delegate to services) - app/services/ — Business logic - app/repositories/ — DB access layer - app/schemas/ — Pydantic v2 request/response models - app/models/ — SQLAlchemy ORM models - app/deps.py — Shared dependencies - tests/ — Mirrors app/ structure ## Code Rules - Type annotations on all function signatures — no exceptions - All routes must be async; no sync I/O inside async functions - Services never import from routers; repositories never import from services - Use HTTPException with specific status codes — never bare 500 - Raise domain exceptions in services; convert to HTTP exceptions in routes ## Database - Use async sessions only (`AsyncSession`) - Wrap multi-step operations in `async with session.begin():` - Never call `session.commit()` inside a repository — let the route handle it - Use alembic for all schema changes — never hand-edit the DB ## What Claude Should Not Do - Don't add print() statements - Don't use Optional[X] — use X | None - Don't nest try/except blocks more than one level deep - Don't create new utility modules — check if the function belongs in an existing one