Django has its own opinions about almost everything — project layout, ORM conventions, migration discipline, the admin interface, signal patterns. When Claude Code encounters a Django codebase without explicit guidance, it produces code that works in a vacuum but violates the conventions your team has spent years establishing.
We reviewed CLAUDE.md files from Django teams running Claude Code in production and identified the patterns that make the most measurable difference. The failures follow a predictable shape: Claude generates python manage.py instead of using the project’s virtualenv, writes raw SQL in views instead of using the ORM, creates new migrations that squash instead of append, and adds inline db_index=True without following the project’s naming conventions.
None of these require workarounds. They require documentation. A well-structured CLAUDE.md for Django closes the gap between what Claude Code guesses and what your project actually needs.
What Django Projects Need in CLAUDE.md (Beyond Generic Python Rules)
A generic Python CLAUDE.md covers formatter choice, import style, and type annotation policy. Django projects need all of that — plus instructions about the ORM, migrations, the management command ecosystem, and the project-specific configuration files that Django loads at startup.
The four areas where AI agents diverge from Django conventions most consistently:
1. Migration discipline. Django migrations are append-only. A correct CLAUDE.md prohibits squashing existing migrations, altering applied migrations, or generating migrations that touch historical entries. Claude without guidance will sometimes suggest squashing “for cleanliness” — advice that breaks every deployment targeting a database that ran the original migrations.
2. Environment and settings. Django loads configuration from a settings module specified by DJANGO_SETTINGS_MODULE. Projects with environment-specific settings files (settings/production.py, settings/local.py) require Claude to use the correct settings path. Running manage.py without the correct --settings flag or environment variable produces wrong behavior.
3. ORM vs raw SQL. Django’s ORM covers the vast majority of production query patterns. CLAUDE.md should explicitly disallow raw SQL (Model.objects.raw(), connection.cursor()) except in named contexts where it’s intentional, such as analytics queries or PostgreSQL-specific operations.
4. Testing patterns. Django supports both unittest.TestCase and pytest-django. Projects pick one and commit to it. Without explicit instruction, Claude mixes self.client calls with pytest fixtures, or uses TestCase setup when the project uses conftest.py-based fixtures.
Project Structure Conventions Claude Code Should Know
The first section of your CLAUDE.md should describe the project layout and the commands Claude needs to run correctly.
# CLAUDE.md — Django Project Instructions
## Project Overview
- **Framework**: Django 5.1
- **Python**: 3.12 (managed via pyenv — see .python-version)
- **Database**: PostgreSQL 16
- **Cache**: Redis 7 (also used as Celery broker)
- **API**: Django REST Framework 3.15
- **Background Tasks**: Celery 5.4 + django-celery-beat (scheduled tasks)
- **Testing**: pytest-django + factory_boy
- **Linting**: ruff + mypy (strict mode)
## Commands
```bash
# Always activate virtualenv first
source .venv/bin/activate
# OR use the project's task runner:
just test # runs pytest
just lint # runs ruff + mypy
just migrate # runs migrate with correct settings
# Direct commands (if not using just):
python manage.py runserver --settings=config.settings.local
python manage.py shell --settings=config.settings.local
python manage.py migrate --settings=config.settings.local
python manage.py check --settings=config.settings.local
# Celery worker (separate terminal)
celery -A config worker -l info
# Celery beat (for scheduled tasks)
celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
Django Settings Structure
Settings are split across config/settings/:
base.py— shared settings (all environments inherit this)local.py— local development (DEBUG=True, local DB)production.py— production (read from environment variables)test.py— test-specific overrides
Set DJANGO_SETTINGS_MODULE=config.settings.local in your shell before running
manage.py without --settings. Never assume base.py is usable standalone.
## Model and Migration Rules
Migration discipline is the most operationally critical part of a Django CLAUDE.md. A single incorrect migration in a long-lived codebase can cause irreversible divergence between the migration history and the actual schema.
```markdown
## Models and Migrations
### Migration Rules (Non-Negotiable)
Migrations are append-only. The following are prohibited:
- DO NOT edit or delete existing migration files
- DO NOT squash migrations without a dedicated release cycle and explicit approval
- DO NOT generate a migration that alters a field in a historical migration file
- DO NOT use `--fake` or manually mark migrations as applied without documenting why
### Generating Migrations
Always use `makemigrations --check` first to verify the current state, then generate:
```bash
python manage.py makemigrations --check --settings=config.settings.local
python manage.py makemigrations app_name --settings=config.settings.local
python manage.py migrate --settings=config.settings.local
Name migrations explicitly when the auto-generated name is unclear:
python manage.py makemigrations app_name --name="add_published_at_to_articles"
Model Conventions
- Use
UUIDFieldas the primary key for all new models - Soft deletes via
django-soft-delete— never use.delete()on production models without checking whether SoftDeleteModel is in the MRO - All
ForeignKeyandManyToManyFielddeclarations must includerelated_name db_index=Trueis set on ForeignKeys by default — avoid redundantMeta.indexesunless covering a specific multi-column pattern- Add
__str__to every model - Custom managers go in
managers.pyinside the app, not inline on the model
QuerySet Conventions
Use the ORM. Raw SQL is permitted only in:
apps/analytics/— complex aggregation queries documented in a docstringapps/reports/— PostgreSQL-specific window functions
Outside these directories, Model.objects.raw() and connection.cursor() are prohibited.
# Good
Article.objects.filter(author=user, status="published").select_related("author")
# Bad — raw SQL for something the ORM handles
Article.objects.raw(
"SELECT * FROM articles WHERE author_id = %s AND status = 'published'", [user.pk]
)
## Test Patterns for Django (pytest-django vs unittest)
```markdown
## Testing
### Framework: pytest-django
All tests use `pytest-django`. Never write `unittest.TestCase` subclasses for new tests.
The existing `TestCase` subclasses in `apps/legacy/` are preserved as-is.
### Configuration: conftest.py
Database access requires the `@pytest.mark.django_db` marker or `db` fixture.
Use `transactional_db` for tests that require transaction rollbacks.
```python
# Good
@pytest.mark.django_db
def test_article_published_count(user_factory):
user_factory.create_batch(3)
assert Article.objects.published().count() == 0
Factories: factory_boy
Never use fixtures files (*.json, *.yaml). All test data is created via factory_boy.
# apps/articles/factories.py
import factory
from factory.django import DjangoModelFactory
from .models import Article
class ArticleFactory(DjangoModelFactory):
class Meta:
model = Article
title = factory.Faker("sentence", nb_words=6)
body = factory.Faker("paragraphs", nb=3, ext_word_list=None)
status = "draft"
author = factory.SubFactory("apps.users.factories.UserFactory")
API Tests: DRF APIClient
Use APIClient for DRF endpoint tests.
from rest_framework.test import APIClient
@pytest.fixture
def api_client():
return APIClient()
@pytest.mark.django_db
def test_article_list_returns_published_only(api_client, user_factory, article_factory):
user = user_factory()
article_factory(status="published", author=user)
article_factory(status="draft", author=user)
api_client.force_authenticate(user=user)
response = api_client.get("/api/v1/articles/")
assert response.status_code == 200
assert len(response.data["results"]) == 1
Coverage
Run pytest --cov=apps --cov-report=term-missing. Coverage threshold is 85%.
Do not lower it. Coverage for new code must meet the threshold before merging.
## Django REST Framework Conventions
```markdown
## Django REST Framework (DRF)
### Serializer Rules
- Always declare fields explicitly. Never use `fields = "__all__"` in production serializers.
- Read-only fields must be listed in `read_only_fields` or set via `ReadOnlyField`.
- Nested serializers for related objects — do not use `depth = N` on ModelSerializer.
```python
class ArticleSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
author_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source="author", write_only=True
)
class Meta:
model = Article
fields = ["id", "title", "body", "status", "author", "author_id", "created_at"]
read_only_fields = ["id", "created_at"]
ViewSet Structure
Use ModelViewSet with explicit queryset and serializer_class. Override
get_queryset() for user-scoped data. Never filter in the serializer.
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticated, IsArticleAuthorOrReadOnly]
def get_queryset(self):
return Article.objects.filter(author=self.request.user).select_related("author")
def perform_create(self, serializer):
serializer.save(author=self.request.user)
URL Routing
Register ViewSets with a DefaultRouter. API URLs are defined in config/urls.py
under the /api/v1/ prefix.
from rest_framework.routers import DefaultRouter
from apps.articles.views import ArticleViewSet
router = DefaultRouter()
router.register(r"articles", ArticleViewSet, basename="article")
Authentication and Permissions
- Default authentication:
JWTAuthentication(simplejwt) - Default permission:
IsAuthenticated - Custom permissions live in
apps/common/permissions.py - Never set
permission_classes = []without a comment explaining why
## Complete CLAUDE.md Template for Django + DRF + Celery + PostgreSQL
Here is the full production-ready template combining all sections above. This covers a Django 5.1 project with DRF 3.15, Celery 5.4, PostgreSQL 16, and pytest-django.
```markdown
# CLAUDE.md — Django API Project
## Stack
- Django 5.1 / DRF 3.15 / Python 3.12
- PostgreSQL 16 / Redis 7
- Celery 5.4 + django-celery-beat
- pytest-django + factory_boy
- ruff + mypy (strict) + pre-commit
## Commands
```bash
source .venv/bin/activate
just test # pytest --cov=apps
just lint # ruff check + mypy
just migrate # migrate --settings=config.settings.local
python manage.py check --settings=config.settings.local
celery -A config worker -l info
Migration Rules
Migrations are append-only. Never edit or squash existing migration files.
Always run makemigrations --check before generating new migrations.
Name migrations explicitly: --name="add_field_to_model".
Model Conventions
- Primary keys: UUIDField
- Soft deletes: SoftDeleteModel from django-soft-delete. Never call .delete() without checking the MRO. Use .soft_delete() instead.
- ForeignKey and ManyToManyField always include related_name
- Add str to every model
Query Conventions
Use the ORM exclusively in apps outside of apps/analytics/ and apps/reports/. Raw SQL in those directories requires a docstring explaining why ORM is insufficient. Banned outside analytics/reports: Model.objects.raw(), connection.cursor()
DRF Conventions
- Never use fields = “all” in serializers
- Nest serializers for read, use PrimaryKeyRelatedField for writes
- Filter data in get_queryset(), never in serializers
- Permission classes default: [IsAuthenticated]
- Authentication: JWTAuthentication (simplejwt)
- Router prefix: /api/v1/
Celery Task Conventions
- Tasks live in apps/<app_name>/tasks.py
- Always use bind=True and self.retry() for network-dependent tasks
- Use shared_task for reusable tasks outside an app context
- Chord and group usage: document intent in a docstring
- Never call tasks synchronously (.delay() or .apply_async() only)
- Retry policy: max_retries=3, countdown=60 (exponential backoff via countdown=60 * 2**self.request.retries)
Testing
- pytest-django only. No unittest.TestCase for new tests.
- factory_boy for all test data. No fixture files.
- @pytest.mark.django_db for DB access. Use transactional_db for rollback tests.
- API tests via APIClient, not Django’s test client.
Code Style
- ruff for linting and formatting. ruff format replaces black.
- mypy —strict for type checking. No
# type: ignorewithout a comment. - Import order: ruff-isort (managed automatically).
Secrets
Never commit secrets. Use django-environ to read from .env (not committed).
For Claude Code environments, use op run to inject secrets at runtime.
## Claude Code Hooks for Django
Claude Code's [hooks system](/blog/claude-code-hooks-automation-guide-2026) provides a way to run `manage.py check` automatically after file edits — catching configuration errors, missing migrations, and deployment blockers before Claude moves to the next task.
### PostToolUse Hook: Automatic System Check
Add this to your project's `.claude/settings.json`:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "source .venv/bin/activate && python manage.py check --settings=config.settings.local 2>&1 | tail -5"
}
]
}
]
}
}
This runs manage.py check after every file edit. The system check catches:
- Missing required settings
- Deprecated configuration
- Database misconfiguration
- App registration errors (missing
INSTALLED_APPSentry)
PostToolUse Hook: Migration State Verification
After Claude generates or edits a model, verify that the migration state is consistent:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"if": "tool_input.file_path =~ /models\\.py$/",
"hooks": [
{
"type": "command",
"command": "source .venv/bin/activate && python manage.py makemigrations --check --settings=config.settings.local 2>&1 | tail -3"
}
]
}
]
}
}
The --check flag returns a non-zero exit code if unapplied model changes exist. When this hook fires after a model edit, Claude Code sees the output and knows to generate a migration before continuing.
PreToolUse Hook: Block Prohibited SQL
For projects that enforce ORM-only in application code, a pre-edit hook can warn when Claude is about to write raw SQL in a prohibited location:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"if": "tool_input.new_string contains 'objects.raw(' or tool_input.new_string contains 'connection.cursor'",
"hooks": [
{
"type": "command",
"command": "echo 'WARNING: Raw SQL detected outside analytics/reports. Use ORM unless this is an intentional exception.'"
}
]
}
]
}
}
This is advisory, not blocking — it produces output that Claude Code reads before executing the edit. In practice, Claude will reconsider and use the ORM approach when it sees the warning.
FAQ
Should I put the full Django settings in CLAUDE.md?
No. CLAUDE.md should describe the settings structure and the DJANGO_SETTINGS_MODULE value to use for each environment. The actual settings live in config/settings/. What belongs in CLAUDE.md is: which settings file to use per command, where secrets come from, and which environment variables the project requires. If Claude runs manage.py with the wrong settings module, it will load wrong database credentials or miss required environment variables.
How do I handle Django apps across a monorepo in CLAUDE.md?
Use path-scoped rules in .claude/rules/. Create one rule file per Django app that contains app-specific conventions: which models the app owns, its API namespace, its Celery task categories. A global CLAUDE.md at the repo root handles shared conventions (migrations, ORM policy, testing framework), and per-app rules handle domain-specific patterns. See Claude Code’s CLAUDE.md hierarchy guide for the directory structure.
Do migrations need to be mentioned in CLAUDE.md even if we review them in PR?
Yes. PR review catches migration errors after Claude generates them. CLAUDE.md prevents them from being generated incorrectly in the first place. The most common migration errors — squashing, editing historical files, missing --name flags — are easier to avoid through upfront instruction than to catch in review. Claude Code benefits from explicit migration policy because the consequences of a wrong migration are asymmetric: easy to avoid, costly to fix.
How should I configure CLAUDE.md for Django’s apps/ layout vs a flat layout?
Document the import path convention. Django projects frequently use either from apps.articles.models import Article or from articles.models import Article depending on whether apps/ is a Python package or a directory. Claude gets this wrong when the convention isn’t stated. In CLAUDE.md, show one import example for a model, a task, and a serializer. That anchors the import style for all generated code.
Should Celery tasks be typed in CLAUDE.md?
Declare at minimum: where tasks live (filename pattern), retry policy defaults, and whether the project uses shared_task or class-based tasks. Celery’s task decorator has many options, and Claude will pick defaults that may conflict with your Celery configuration. Documenting bind=True, max_retries, and countdown prevents AI-generated tasks from silently ignoring failures or blocking workers.
Can CLAUDE.md prevent Django admin-related issues?
Yes. Two common patterns to document: which apps register admin classes (and which intentionally don’t), and whether the project uses a custom AdminSite. Claude sometimes generates admin.site.register(Model) in apps that use a custom site class. A one-line note — “This project uses a custom AdminSite at config.admin.admin_site. Register models with admin_site.register(), not admin.site.register().” — prevents the issue.
What’s the right approach for CLAUDE.md in a Django project with both DRF and GraphQL?
Separate the API conventions by technology. Use path-scoped rules: a drf.md rule that loads for apps/*/views.py and apps/*/serializers.py, and a graphql.md rule that loads for apps/*/schema.py and apps/*/mutations.py. The root CLAUDE.md declares which API technology owns which URL prefix and which directories to use for each. Claude Code then applies the correct convention set based on the files in context.
Related Articles
- CLAUDE.md for Python Projects: Complete Template Guide
- Claude Code Hooks: Automate Pre/Post Tool Actions
- AGENTS.md for FastAPI: Complete Patterns Guide
- Browse AI coding rule templates in our gallery
Keep Django Secrets Out of Your Codebase
Django projects accumulate secrets quickly: database credentials, secret keys, third-party API tokens, Celery broker URLs. 1Password CLI’s op run injects these into your shell environment at runtime — no .env files committed, no secrets in command history, and full audit logs for every secret Claude Code’s hooks or management commands access.