// 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--path=
AGENTS.md - OpenAI Codex--path=
AGENTS.md - Windsurf--path=
AGENTS.md - Cline--path=
AGENTS.md
// File Content
AGENTS.md
1# AGENTS.md — Python + Django23This file contains your working instructions for this codebase. Follow these4conventions, workflow rules, and behavioral guidelines on every task.56---78## Quick Reference910| 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) |2930Always run `ruff check . && ruff format . --check && pytest` before committing.3132---3334## Repo Structure3536```37├── config/ # Project-level configuration38│ ├── __init__.py39│ ├── settings/40│ │ ├── __init__.py # Imports from base, overrides per environment41│ │ ├── base.py # Shared settings (all environments)42│ │ ├── local.py # Local dev overrides43│ │ ├── test.py # Test-specific settings44│ │ └── production.py # Production settings (reads from env vars)45│ ├── urls.py # Root URL configuration46│ ├── wsgi.py47│ ├── asgi.py48│ └── celery.py # Celery app (if present)49├── apps/50│ └── <app_name>/51│ ├── __init__.py52│ ├── admin.py53│ ├── apps.py54│ ├── models.py # Or models/ package for large apps55│ ├── services.py # Business logic lives here, NOT in views56│ ├── selectors.py # Complex read queries (optional pattern)57│ ├── views.py # Thin — delegates to services58│ ├── serializers.py # DRF serializers (if using DRF)59│ ├── urls.py # App-level URL patterns60│ ├── tasks.py # Celery tasks (if present)61│ ├── signals.py # Signal handlers (use sparingly)62│ ├── managers.py # Custom QuerySet/Manager classes63│ ├── constants.py # App-specific constants and enums64│ └── migrations/65├── templates/66│ ├── base.html67│ └── <app_name>/68├── static/69│ └── <app_name>/70├── tests/71│ ├── conftest.py # Shared fixtures72│ ├── factories.py # Factory Boy factories (or per-app)73│ └── <app_name>/74│ ├── __init__.py75│ ├── test_models.py76│ ├── test_services.py77│ ├── test_views.py78│ └── test_tasks.py79├── manage.py80├── pyproject.toml81├── .env.example # Template — never commit real .env82└── docker-compose.yml # Local dev services (Postgres, Redis)83```8485If 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.8687---8889## Settings & Environment9091### Settings Split9293Settings are split by environment. `DJANGO_SETTINGS_MODULE` controls which file loads:9495- **`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.99100### Environment Variables101102All secrets and host-specific values come from environment variables. Use `django-environ` or `os.environ` with explicit lookups — never hardcode credentials.103104Required env vars are documented in `.env.example`. Common ones:105106```107DJANGO_SETTINGS_MODULE=config.settings.local108DATABASE_URL=postgres://user:pass@localhost:5432/dbname109SECRET_KEY=change-me110ALLOWED_HOSTS=localhost,127.0.0.1111REDIS_URL=redis://localhost:6379/0112```113114**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.118119---120121## Architecture Conventions122123### Models124125- 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.133134### Service Layer135136Business logic lives in `services.py`, not in views, serializers, or model methods.137138```python139# apps/orders/services.py140141def place_order(*, user: User, items: list[OrderItem], shipping_address: Address) -> Order:142 """Create an order, charge payment, and send confirmation email."""143 ...144```145146**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.152153### Selectors (Optional)154155For complex read queries, use `selectors.py` to separate read logic from write logic:156157```python158# apps/orders/selectors.py159160def get_pending_orders(*, user: User) -> QuerySet[Order]:161 return Order.objects.filter(user=user, status=Order.Status.PENDING).select_related("shipping_address")162```163164This pattern is optional. For simple apps, querysets in views or services are fine.165166### Views167168Views are thin. They handle HTTP concerns (authentication, permissions, request parsing, response formatting) and delegate to services/selectors.169170```python171# Correct: view delegates to service172def create_order(request):173 order = place_order(user=request.user, items=..., shipping_address=...)174 return redirect("orders:detail", pk=order.pk)175176# Wrong: business logic in the view177def create_order(request):178 order = Order.objects.create(...)179 charge_payment(order) # This belongs in a service180 send_email(order) # This too181 return redirect(...)182```183184### DRF (if present)185186If 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.193194### Background Jobs (if Celery is configured)195196- 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.201202```python203@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 function208 except Order.DoesNotExist:209 return # Object deleted, don't retry210 except EmailSendError as exc:211 self.retry(exc=exc)212```213214### Signals215216Use signals sparingly. They make control flow hard to follow. Acceptable uses:217- Cache invalidation218- Denormalized counter updates219- Audit logging220221Do not use signals for business logic (e.g., sending emails on save). Put that in a service.222223### Logging224225- 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.230231---232233## Migrations234235### General Rules236237- 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.242243### Data Migrations244245Use `RunPython` for data migrations. Always include a reverse function (or `migrations.RunPython.noop` if irreversible):246247```python248def 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"])253254class Migration(migrations.Migration):255 operations = [256 migrations.RunPython(populate_slug, migrations.RunPython.noop),257 ]258```259260**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).264265### Safe Migration Practices266267When 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.272273---274275## Testing276277### Setup278279Tests use `pytest` with `pytest-django`. Configuration lives in `pyproject.toml`:280281```toml282[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```292293### Fixtures & Factories294295Use `factory_boy` for test data. Define factories in `tests/factories.py` or `tests/<app>/factories.py`:296297```python298class UserFactory(factory.django.DjangoModelFactory):299 class Meta:300 model = "auth.User"301302 username = factory.Sequence(lambda n: f"user-{n}")303 email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")304 is_active = True305```306307Use `conftest.py` for shared pytest fixtures:308309```python310@pytest.fixture311def user(db):312 return UserFactory()313314@pytest.fixture315def api_client():316 return APIClient()317318@pytest.fixture319def authenticated_client(user, api_client):320 api_client.force_authenticate(user=user)321 return api_client322```323324### Testing Conventions325326- **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`.334335### What to Test When Making Changes336337- 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.341342---343344## API Backwards Compatibility345346If the project exposes an API:347348- **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.353354---355356## Code Style357358- 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.365366---367368## Django-Specific Pitfalls369370These are common mistakes. Avoid them:371372- **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.379380---381382## Pull Request Checklist383384Before submitting any change:3853861. `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