AGENTS.md — Python + Django
Cross-tool AGENTS.md for Django projects with service-layer architecture, migrations, and testing standards.
Install path
Use this file for each supported tool in your project.
- Cursor: Save as
AGENTS.mdin your project atAGENTS.md. - Claude Code: Save as
AGENTS.mdin your project atAGENTS.md. - OpenAI Codex: Save as
AGENTS.mdin your project atAGENTS.md. - Windsurf: Save as
AGENTS.mdin your project atAGENTS.md. - Cline: Save as
AGENTS.mdin your project atAGENTS.md.
Configuration
AGENTS.md
123# AGENTS.md — Python + Django45## Quick Reference67| 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) |2627Always run `ruff check . && ruff format . --check && pytest` before committing.2829---3031## Repo Structure3233```34├── config/ # Project-level configuration35│ ├── __init__.py36│ ├── settings/37│ │ ├── __init__.py # Imports from base, overrides per environment38│ │ ├── base.py # Shared settings (all environments)39│ │ ├── local.py # Local dev overrides40│ │ ├── test.py # Test-specific settings41│ │ └── production.py # Production settings (reads from env vars)42│ ├── urls.py # Root URL configuration43│ ├── wsgi.py44│ ├── asgi.py45│ └── celery.py # Celery app (if present)46├── apps/47│ └── <app_name>/48│ ├── __init__.py49│ ├── admin.py50│ ├── apps.py51│ ├── models.py # Or models/ package for large apps52│ ├── services.py # Business logic lives here, NOT in views53│ ├── selectors.py # Complex read queries (optional pattern)54│ ├── views.py # Thin — delegates to services55│ ├── serializers.py # DRF serializers (if using DRF)56│ ├── urls.py # App-level URL patterns57│ ├── tasks.py # Celery tasks (if present)58│ ├── signals.py # Signal handlers (use sparingly)59│ ├── managers.py # Custom QuerySet/Manager classes60│ ├── constants.py # App-specific constants and enums61│ └── migrations/62├── templates/63│ ├── base.html64│ └── <app_name>/65├── static/66│ └── <app_name>/67├── tests/68│ ├── conftest.py # Shared fixtures69│ ├── factories.py # Factory Boy factories (or per-app)70│ └── <app_name>/71│ ├── __init__.py72│ ├── test_models.py73│ ├── test_services.py74│ ├── test_views.py75│ └── test_tasks.py76├── manage.py77├── pyproject.toml78├── .env.example # Template — never commit real .env79└── docker-compose.yml # Local dev services (Postgres, Redis)80```8182If 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.8384---8586## Settings & Environment8788### Settings Split8990Settings are split by environment. `DJANGO_SETTINGS_MODULE` controls which file loads:9192- **`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.9697### Environment Variables9899All secrets and host-specific values come from environment variables. Use `django-environ` or `os.environ` with explicit lookups — never hardcode credentials.100101Required env vars are documented in `.env.example`. Common ones:102103```104DJANGO_SETTINGS_MODULE=config.settings.local105DATABASE_URL=postgres://user:pass@localhost:5432/dbname106SECRET_KEY=change-me107ALLOWED_HOSTS=localhost,127.0.0.1108REDIS_URL=redis://localhost:6379/0109```110111**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.115116---117118## Architecture Conventions119120### Models121122- 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.130131### Service Layer132133Business logic lives in `services.py`, not in views, serializers, or model methods.134135```python136# apps/orders/services.py137138def place_order(*, user: User, items: list[OrderItem], shipping_address: Address) -> Order:139 """Create an order, charge payment, and send confirmation email."""140 ...141```142143**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.149150### Selectors (Optional)151152For complex read queries, use `selectors.py` to separate read logic from write logic:153154```python155# apps/orders/selectors.py156157def get_pending_orders(*, user: User) -> QuerySet[Order]:158 return Order.objects.filter(user=user, status=Order.Status.PENDING).select_related("shipping_address")159```160161This pattern is optional. For simple apps, querysets in views or services are fine.162163### Views164165Views are thin. They handle HTTP concerns (authentication, permissions, request parsing, response formatting) and delegate to services/selectors.166167```python168# Correct: view delegates to service169def create_order(request):170 order = place_order(user=request.user, items=..., shipping_address=...)171 return redirect("orders:detail", pk=order.pk)172173# Wrong: business logic in the view174def create_order(request):175 order = Order.objects.create(...)176 charge_payment(order) # This belongs in a service177 send_email(order) # This too178 return redirect(...)179```180181### DRF (if present)182183If 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.190191### Background Jobs (if Celery is configured)192193- 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.198199```python200@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 function205 except Order.DoesNotExist:206 return # Object deleted, don't retry207 except EmailSendError as exc:208 self.retry(exc=exc)209```210211### Signals212213Use signals sparingly. They make control flow hard to follow. Acceptable uses:214- Cache invalidation215- Denormalized counter updates216- Audit logging217218Do not use signals for business logic (e.g., sending emails on save). Put that in a service.219220### Logging221222- 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.227228---229230## Migrations231232### General Rules233234- 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.239240### Data Migrations241242Use `RunPython` for data migrations. Always include a reverse function (or `migrations.RunPython.noop` if irreversible):243244```python245def 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"])250251class Migration(migrations.Migration):252 operations = [253 migrations.RunPython(populate_slug, migrations.RunPython.noop),254 ]255```256257**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).261262### Safe Migration Practices263264When 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.269270---271272## Testing273274### Setup275276Tests use `pytest` with `pytest-django`. Configuration lives in `pyproject.toml`:277278```toml279[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```289290### Fixtures & Factories291292Use `factory_boy` for test data. Define factories in `tests/factories.py` or `tests/<app>/factories.py`:293294```python295class UserFactory(factory.django.DjangoModelFactory):296 class Meta:297 model = "auth.User"298299 username = factory.Sequence(lambda n: f"user-{n}")300 email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")301 is_active = True302```303304Use `conftest.py` for shared pytest fixtures:305306```python307@pytest.fixture308def user(db):309 return UserFactory()310311@pytest.fixture312def api_client():313 return APIClient()314315@pytest.fixture316def authenticated_client(user, api_client):317 api_client.force_authenticate(user=user)318 return api_client319```320321### Testing Conventions322323- **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`.331332### What to Test When Making Changes333334- 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.338339---340341## API Backwards Compatibility342343If the project exposes an API:344345- **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.350351---352353## Code Style354355- 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.362363---364365## Django-Specific Pitfalls366367These are common mistakes. Avoid them:368369- **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.376377---378379## Pull Request Checklist380381Before submitting any change:3823831. `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: