dotmd

AGENTS.md — Python + Django

Cross-tool AGENTS.md for Django projects with service-layer architecture, migrations, and testing standards.

By dotmd TeamCC0Published Feb 19, 2026View source ↗

Install path

Use this file for each supported tool in your project.

  • Cursor: Save as AGENTS.md in your project at AGENTS.md.
  • Claude Code: Save as AGENTS.md in your project at AGENTS.md.
  • OpenAI Codex: Save as AGENTS.md in your project at AGENTS.md.
  • Windsurf: Save as AGENTS.md in your project at AGENTS.md.
  • Cline: Save as AGENTS.md in your project at AGENTS.md.

Configuration

AGENTS.md

1
2
3# AGENTS.md — Python + Django
4
5## Quick Reference
6
7| Task | Command |
8|---|---|
9| Install dependencies | `pip install -e ".[dev]"` or `poetry install` or `uv sync` |
10| Run dev server | `python manage.py runserver` |
11| Run all tests | `pytest` |
12| Run single test | `pytest tests/app_name/test_module.py::test_func -x` |
13| Run tests (parallel) | `pytest -n auto` |
14| Lint | `ruff check .` |
15| Lint (fix) | `ruff check . --fix` |
16| Format | `ruff format .` |
17| Format (check only) | `ruff format . --check` |
18| Type check | `mypy .` |
19| Make migrations | `python manage.py makemigrations` |
20| Run migrations | `python manage.py migrate` |
21| Show migration plan | `python manage.py showmigrations` |
22| Shell | `python manage.py shell_plus` (if django-extensions installed) |
23| Create superuser | `python manage.py createsuperuser` |
24| Celery worker | `celery -A config worker -l info` (if Celery is configured) |
25| Celery beat | `celery -A config beat -l info` (if Celery is configured) |
26
27Always run `ruff check . && ruff format . --check && pytest` before committing.
28
29---
30
31## Repo Structure
32
33```
34├── config/ # Project-level configuration
35│ ├── __init__.py
36│ ├── settings/
37│ │ ├── __init__.py # Imports from base, overrides per environment
38│ │ ├── base.py # Shared settings (all environments)
39│ │ ├── local.py # Local dev overrides
40│ │ ├── test.py # Test-specific settings
41│ │ └── production.py # Production settings (reads from env vars)
42│ ├── urls.py # Root URL configuration
43│ ├── wsgi.py
44│ ├── asgi.py
45│ └── celery.py # Celery app (if present)
46├── apps/
47│ └── <app_name>/
48│ ├── __init__.py
49│ ├── admin.py
50│ ├── apps.py
51│ ├── models.py # Or models/ package for large apps
52│ ├── services.py # Business logic lives here, NOT in views
53│ ├── selectors.py # Complex read queries (optional pattern)
54│ ├── views.py # Thin — delegates to services
55│ ├── serializers.py # DRF serializers (if using DRF)
56│ ├── urls.py # App-level URL patterns
57│ ├── tasks.py # Celery tasks (if present)
58│ ├── signals.py # Signal handlers (use sparingly)
59│ ├── managers.py # Custom QuerySet/Manager classes
60│ ├── constants.py # App-specific constants and enums
61│ └── migrations/
62├── templates/
63│ ├── base.html
64│ └── <app_name>/
65├── static/
66│ └── <app_name>/
67├── tests/
68│ ├── conftest.py # Shared fixtures
69│ ├── factories.py # Factory Boy factories (or per-app)
70│ └── <app_name>/
71│ ├── __init__.py
72│ ├── test_models.py
73│ ├── test_services.py
74│ ├── test_views.py
75│ └── test_tasks.py
76├── manage.py
77├── pyproject.toml
78├── .env.example # Template — never commit real .env
79└── docker-compose.yml # Local dev services (Postgres, Redis)
80```
81
82If the repo uses a different layout (e.g., apps at the root, a `src/` directory, or a monorepo), follow whatever structure is already established. Do not reorganize.
83
84---
85
86## Settings & Environment
87
88### Settings Split
89
90Settings are split by environment. `DJANGO_SETTINGS_MODULE` controls which file loads:
91
92- **`base.py`** — everything shared: `INSTALLED_APPS`, middleware, database engine, auth backends, logging shape, `LANGUAGE_CODE`, `TIME_ZONE = "UTC"`. No secrets. No host-specific values.
93- **`local.py`** — `DEBUG = True`, `django-debug-toolbar`, `INTERNAL_IPS`, relaxed `ALLOWED_HOSTS`, console email backend.
94- **`test.py`** — `PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]` for speed, in-memory cache, `EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"`.
95- **`production.py`** — everything from env vars. `DEBUG` is always `False`. No defaults for secrets.
96
97### Environment Variables
98
99All secrets and host-specific values come from environment variables. Use `django-environ` or `os.environ` with explicit lookups — never hardcode credentials.
100
101Required env vars are documented in `.env.example`. Common ones:
102
103```
104DJANGO_SETTINGS_MODULE=config.settings.local
105DATABASE_URL=postgres://user:pass@localhost:5432/dbname
106SECRET_KEY=change-me
107ALLOWED_HOSTS=localhost,127.0.0.1
108REDIS_URL=redis://localhost:6379/0
109```
110
111**Rules:**
112- Never commit `.env` files. `.env.example` has placeholder values only.
113- Never put secrets in settings files, even `local.py`.
114- Production secrets come from the deployment platform's secret manager, not env files on disk.
115
116---
117
118## Architecture Conventions
119
120### Models
121
122- One model per concept. Keep models focused on data shape and database-level constraints.
123- Use `choices` with `models.TextChoices` or `models.IntegerChoices` for enumerated fields.
124- Always set `class Meta: ordering`, `__str__`, and `verbose_name`/`verbose_name_plural` where relevant.
125- Use `UUIDField` as primary key if the model's IDs are exposed in URLs or APIs. Otherwise the default auto-incrementing `id` is fine.
126- Timestamps: include `created_at = models.DateTimeField(auto_now_add=True)` and `updated_at = models.DateTimeField(auto_now=True)` on every model (use an abstract base model).
127- Use `related_name` on every ForeignKey and M2M field. Never rely on Django's default `_set` suffix.
128- Database indexes: add `db_index=True` or `Meta.indexes` for fields you query/filter by frequently. Think about this at model design time.
129- Custom managers go in `managers.py`. Keep the default manager unfiltered.
130
131### Service Layer
132
133Business logic lives in `services.py`, not in views, serializers, or model methods.
134
135```python
136# apps/orders/services.py
137
138def place_order(*, user: User, items: list[OrderItem], shipping_address: Address) -> Order:
139 """Create an order, charge payment, and send confirmation email."""
140 ...
141```
142
143**Rules:**
144- Services are plain functions (not classes) unless there's a compelling reason.
145- Use keyword-only arguments (`*`) for services to force named parameters at call sites.
146- Services call other services; views call services. Models don't call services.
147- Services raise domain exceptions (defined in the app), not DRF/HTTP exceptions.
148- Keep services testable: they take explicit arguments, not request objects.
149
150### Selectors (Optional)
151
152For complex read queries, use `selectors.py` to separate read logic from write logic:
153
154```python
155# apps/orders/selectors.py
156
157def get_pending_orders(*, user: User) -> QuerySet[Order]:
158 return Order.objects.filter(user=user, status=Order.Status.PENDING).select_related("shipping_address")
159```
160
161This pattern is optional. For simple apps, querysets in views or services are fine.
162
163### Views
164
165Views are thin. They handle HTTP concerns (authentication, permissions, request parsing, response formatting) and delegate to services/selectors.
166
167```python
168# Correct: view delegates to service
169def create_order(request):
170 order = place_order(user=request.user, items=..., shipping_address=...)
171 return redirect("orders:detail", pk=order.pk)
172
173# Wrong: business logic in the view
174def create_order(request):
175 order = Order.objects.create(...)
176 charge_payment(order) # This belongs in a service
177 send_email(order) # This too
178 return redirect(...)
179```
180
181### DRF (if present)
182
183If Django REST Framework is in use:
184- Serializers handle validation and data shaping. They do not contain business logic.
185- Use `ModelSerializer` for simple CRUD; plain `Serializer` for complex input validation.
186- ViewSets for standard CRUD resources; `@api_view` or `APIView` for custom endpoints.
187- Versioning: use URL path versioning (`/api/v1/...`) if API is public.
188- Pagination: set `DEFAULT_PAGINATION_CLASS` in settings — do not paginate ad-hoc per view.
189- Permissions: use DRF permission classes, not manual checks in view bodies.
190
191### Background Jobs (if Celery is configured)
192
193- Tasks live in `apps/<app_name>/tasks.py`.
194- Tasks are thin wrappers that call services. The service contains the logic, the task handles retry/queue config.
195- Always use `bind=True` and set `max_retries`, `default_retry_delay`.
196- Pass scalar IDs to tasks, not model instances. Re-fetch from DB inside the task.
197- Use `task_always_eager = True` in test settings to run tasks synchronously.
198
199```python
200@shared_task(bind=True, max_retries=3, default_retry_delay=60)
201def send_order_confirmation(self, order_id: int) -> None:
202 try:
203 order = Order.objects.get(id=order_id)
204 send_order_confirmation_email(order=order) # service function
205 except Order.DoesNotExist:
206 return # Object deleted, don't retry
207 except EmailSendError as exc:
208 self.retry(exc=exc)
209```
210
211### Signals
212
213Use signals sparingly. They make control flow hard to follow. Acceptable uses:
214- Cache invalidation
215- Denormalized counter updates
216- Audit logging
217
218Do not use signals for business logic (e.g., sending emails on save). Put that in a service.
219
220### Logging
221
222- Use `structlog` or stdlib `logging` — be consistent with what the project uses.
223- Get loggers per module: `logger = logging.getLogger(__name__)`.
224- Log at appropriate levels: `ERROR` for unexpected failures, `WARNING` for degraded operation, `INFO` for significant state changes, `DEBUG` for development troubleshooting.
225- Never log secrets, tokens, passwords, or full request bodies containing PII.
226- Include contextual identifiers (user_id, order_id) in log messages.
227
228---
229
230## Migrations
231
232### General Rules
233
234- Every model change requires a migration. Never modify models without running `makemigrations`.
235- One migration per logical change. Don't combine unrelated model changes in one migration.
236- After creating a migration, always inspect the generated file. Auto-generated migrations can be wrong (especially for renames vs. drop-and-recreate).
237- Migration files are committed to version control. Never `.gitignore` them.
238- Never edit a migration that has already been applied in production. Create a new one.
239
240### Data Migrations
241
242Use `RunPython` for data migrations. Always include a reverse function (or `migrations.RunPython.noop` if irreversible):
243
244```python
245def populate_slug(apps, schema_editor):
246 Article = apps.get_model("blog", "Article")
247 for article in Article.objects.filter(slug=""):
248 article.slug = slugify(article.title)
249 article.save(update_fields=["slug"])
250
251class Migration(migrations.Migration):
252 operations = [
253 migrations.RunPython(populate_slug, migrations.RunPython.noop),
254 ]
255```
256
257**Rules for data migrations:**
258- Use `apps.get_model()` — never import models directly.
259- Batch large updates with `.iterator()` and `bulk_update()`.
260- Separate data migrations from schema migrations (different files).
261
262### Safe Migration Practices
263
264When working on a deployed project:
265- **Adding a column**: always use `null=True` or provide a `default`. Adding a non-nullable column without a default locks the table and fails on existing rows.
266- **Removing a column**: first remove all code that references it, deploy, then remove the column in a follow-up migration.
267- **Renaming a column**: use `RenameField`, not a drop-and-recreate. Verify the generated migration does a rename, not add+remove.
268- **Adding an index**: use `AddIndex` with `Meta.indexes` on large tables. Consider `CREATE INDEX CONCURRENTLY` via `SeparateDatabaseAndState` for Postgres if the table is large and the project can't tolerate downtime.
269
270---
271
272## Testing
273
274### Setup
275
276Tests use `pytest` with `pytest-django`. Configuration lives in `pyproject.toml`:
277
278```toml
279[tool.pytest.ini_options]
280DJANGO_SETTINGS_MODULE = "config.settings.test"
281python_files = ["test_*.py"]
282python_classes = ["Test*"]
283python_functions = ["test_*"]
284addopts = "-v --tb=short --strict-markers -x"
285markers = [
286 "slow: marks tests as slow (deselect with '-m \"not slow\"')",
287]
288```
289
290### Fixtures & Factories
291
292Use `factory_boy` for test data. Define factories in `tests/factories.py` or `tests/<app>/factories.py`:
293
294```python
295class UserFactory(factory.django.DjangoModelFactory):
296 class Meta:
297 model = "auth.User"
298
299 username = factory.Sequence(lambda n: f"user-{n}")
300 email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")
301 is_active = True
302```
303
304Use `conftest.py` for shared pytest fixtures:
305
306```python
307@pytest.fixture
308def user(db):
309 return UserFactory()
310
311@pytest.fixture
312def api_client():
313 return APIClient()
314
315@pytest.fixture
316def authenticated_client(user, api_client):
317 api_client.force_authenticate(user=user)
318 return api_client
319```
320
321### Testing Conventions
322
323- **Test services**, not implementation details. Services are the primary unit of business logic.
324- **Test views** for HTTP-level concerns: status codes, permissions, response shape.
325- **Test models** for custom methods, properties, constraints, and managers.
326- Use `pytest.mark.django_db` (or fixtures that depend on `db`) for tests that touch the database.
327- Use `freezegun` or `time-machine` for time-dependent tests. Never assert against `datetime.now()`.
328- Mock external services (payment gateways, email providers, third-party APIs). Never call external services in tests.
329- Keep tests fast: use `MD5PasswordHasher` in test settings, minimize database writes, use `select_related`/`prefetch_related` awareness in factories.
330- Name tests descriptively: `test_place_order_with_insufficient_stock_raises_error` over `test_order_fail`.
331
332### What to Test When Making Changes
333
334- Changing a model → test migrations apply cleanly, test model constraints and methods.
335- Changing a service → test the service function directly with various inputs.
336- Changing a view → test HTTP response, permissions, and edge cases.
337- Adding an API endpoint → test all HTTP methods, authentication, permissions, validation errors, and success responses.
338
339---
340
341## API Backwards Compatibility
342
343If the project exposes an API:
344
345- **Never remove a field** from a response without versioning or a deprecation period.
346- **Never rename a field** — add the new name, keep the old one, deprecate.
347- **Never change a field's type** (e.g., string to int).
348- **New required request fields** break clients. Add them as optional with defaults, or version the endpoint.
349- Document breaking changes explicitly if versioning is used.
350
351---
352
353## Code Style
354
355- Follow whatever `ruff` and formatter config exists in `pyproject.toml`. Don't override project rules.
356- Imports: use `ruff` with `isort`-compatible rules. Standard lib → third party → local, separated by blank lines.
357- Strings: double quotes by default (ruff format default). Follow the project's existing convention.
358- Type hints: use them on service functions, selectors, and public APIs. Use `from __future__ import annotations` for modern syntax.
359- Max line length: whatever `ruff` is configured to (default: 88).
360- Docstrings: required on service functions and non-trivial public methods. Use Google style or NumPy style — match the project.
361- No `# noqa` or `# type: ignore` without a comment explaining why.
362
363---
364
365## Django-Specific Pitfalls
366
367These are common mistakes. Avoid them:
368
369- **N+1 queries**: use `select_related` (FK/OneToOne) and `prefetch_related` (M2M/reverse FK). Use `django-debug-toolbar` or `nplusone` to catch them in dev.
370- **Unbounded querysets**: never iterate over `.all()` without `.iterator()` or pagination in production code paths. In management commands processing large datasets, use `.iterator(chunk_size=2000)`.
371- **Mutable default arguments**: never use `default=[]` or `default={}` on model fields. Use `default=list` or `default=dict`.
372- **Circular imports**: use string references for ForeignKey (`"app_label.ModelName"`) and lazy imports in signals/services when needed.
373- **Missing `on_delete`**: always specify it explicitly on ForeignKey. Use `PROTECT` for data you can't afford to cascade-delete.
374- **Time zones**: always use `django.utils.timezone.now()`, never `datetime.now()`. Store everything in UTC.
375- **Transaction safety**: wrap multi-step write operations in `transaction.atomic()`. Use `select_for_update()` when you need row-level locking.
376
377---
378
379## Pull Request Checklist
380
381Before submitting any change:
382
3831. `ruff check . && ruff format . --check` passes with zero issues.
3842. `pytest` passes. No skipped tests without a reason.
3853. New models have migrations. `python manage.py makemigrations --check` shows no pending migrations.
3864. Migration files have been inspected for correctness.
3875. No secrets, credentials, or `.env` files in the diff.
3886. New service functions have tests.
3897. API changes are backwards compatible (or versioned).
3908. N+1 queries have been checked on new view code.
391

Community feedback

0 found this helpful

Works with: