dotmd
// Config Record

>Python + Django

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

author:
dotmd Team
license:CC0
published:Feb 19, 2026
// Installation

>Add this file to your project repository:

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