Stack conventions
Django
Django web applications and APIs (Django 4.2 LTS to 5.x/6.x), on top of the Python profile.
Django web applications and APIs (Django 4.2 LTS to 5.x/6.x), on top of the Python profile.
Used for: django.
Principles
- Follow Django's conventions and the project's existing app layout; reach for built-in features (forms, auth, admin, ORM, messages) before custom code.
- Keep domain rules close to the data - model methods, managers and QuerySet methods - and use a thin service function only for workflows that span several models or external systems.
- Validate all input with forms or serializers (Django REST Framework or Ninja schemas); views orchestrate, they do not validate by hand.
- Query deliberately - select_related for foreign keys, prefetch_related for many-to-many and reverse relations, only/values when you need few columns.
- Every model change ships with a reviewed migration; migrations are committed, deterministic and safe to run while the old code serves traffic.
- Configure per environment from the environment (settings split or django-environ style), with DEBUG off and secrets never committed.
- Let the framework protect you - CSRF middleware, auto-escaping templates, ORM parameterisation, the password hashers; never bypass them casually.
- Wrap multi-step writes in transaction.atomic and run side effects with transaction.on_commit.
- Prefer clear, explicit code over clever code; readability is the first requirement.
- Add type hints to every new public function, method and module-level constant, and keep them truthful.
- Keep functions small and side effects at the edges; pass dependencies in rather than importing global state.
- Raise specific exceptions with a useful message; never silence an exception without handling or logging it.
Conventions
Accessibility
- Render forms with labels ({{ form.field.label_tag }} or the form's default rendering) and show field errors next to inputs with aria-describedby.
- Use semantic HTML in templates, a lang attribute on html, and meaningful link text.
- Make the messages framework output an aria-live region so status messages are announced.
Concurrency
- Use select_for_update inside transaction.atomic for read-modify-write on the same rows; F() expressions for counters.
- Background tasks (Celery, RQ, Dramatiq, or the Django tasks framework where the project uses it) are idempotent and receive primary keys, not model instances.
- In async views, use the async ORM methods (aget, acreate, async for) or sync_to_async; never call blocking code directly.
- Do not block the event loop in async code; use async libraries or run blocking work in a thread.
- Use concurrent.futures for parallel I/O in synchronous code and processes for CPU-bound work.
- Protect shared mutable state with locks or avoid sharing it; prefer passing messages.
- In asyncio, run concurrent tasks under asyncio.TaskGroup (3.11+) with asyncio.timeout, and never drop the reference to a task created with create_task.
Dependencies
- Pin Django to a supported release (prefer the current LTS for long-lived apps) and upgrade with deprecation warnings enabled (python -Wa manage.py test).
- Prefer well-maintained ecosystem packages (DRF, django-filter, django-environ, django-storages) over custom equivalents.
- Add dependencies to the file the repository uses (pyproject.toml, requirements*.txt) with a compatible version range.
- Pin exact versions in lock files or constraints for applications, not in library metadata.
- Prefer well-maintained libraries and avoid adding one for a few lines of code.
- Use the lock tool the repository already uses (uv.lock, poetry.lock, pip-tools requirements) and update it in the same change as the dependency.
Documentation
- Give models and non-obvious fields help_text and verbose_name; document custom management commands with help.
- Keep README setup (migrate, createsuperuser, runserver, settings variables) accurate.
- Give every public module, class and function a docstring that says what it does and what it returns or raises.
- Explain why in comments; the code already says what.
- Update the README or docs when behaviour, configuration or commands change.
Errors
- Use get_object_or_404 in views; raise PermissionDenied or Http404 rather than returning ad-hoc error responses.
- Catch IntegrityError around writes that may race on unique constraints and turn it into a validation error.
- Configure LOGGING so unhandled exceptions are reported (ADMINS mail or an error tracker); DEBUG is never on in production.
- Catch the narrowest exception type that the code can actually handle.
- Chain exceptions with raise ... from err when translating them, so the cause is kept.
- Define a small exception hierarchy per package when callers need to tell failures apart.
- Do not use assert for runtime validation; asserts are removed under -O.
- Use timezone-aware datetimes (datetime.now(timezone.utc)) for timestamps; naive datetimes cause off-by-hours bugs across hosts.
Logging
- Configure the LOGGING dict in settings; use module loggers (logging.getLogger(name)) in apps.
- Keep django.security and django.request loggers at WARNING or above routed to the error tracker.
- Never log request.POST, cookies, or authorization headers.
- Get a module logger with logging.getLogger(name); never configure logging at import time in a library.
- Use lazy formatting (log.info("saved %s", item_id)) rather than f-strings in log calls.
- Never log secrets, tokens, passwords or full personal data.
Naming
- Apps are short plural or domain nouns (orders, billing); models singular CamelCase (Order); related_name set explicitly on foreign keys.
- URL names namespaced by app (app_name = "orders"; reverse("orders:detail", args=[pk])); never hard-code URLs in templates or code.
- Templates under
/templates/ / to avoid name clashes. - Use snake_case for functions, variables and modules, PascalCase for classes and UPPER_SNAKE_CASE for constants.
- Prefix internal helpers with a single underscore; do not use double underscores for privacy.
- Name booleans as questions (is_ready, has_access) and functions after what they return or do.
Performance
- Eliminate N+1 with select_related/prefetch_related (Prefetch objects for filtered prefetches); check with django-debug-toolbar or query-count assertions.
- Add db_index or Meta.indexes for filtered and ordered columns; use constraints (UniqueConstraint, CheckConstraint) for integrity.
- Use exists(), count() and aggregate()/annotate() in the database instead of Python loops; iterator() for large result sets.
- Use bulk_create/bulk_update for batches and F() expressions for atomic updates.
- Cache expensive fragments and views with the cache framework; paginate list endpoints.
- Choose the right data structure (set or dict lookups instead of list scans in loops).
- Avoid repeated database or HTTP calls inside loops; batch or prefetch.
- Measure with a profiler before optimising and keep the readable version unless the gain is real.
Security
- Keep CsrfViewMiddleware enabled; use csrf_exempt only for webhook endpoints with their own signature verification.
- Never build SQL with string formatting in raw(), extra() or cursor.execute; pass params; avoid extra() entirely.
- Never mark user content safe (mark_safe, |safe, autoescape off); use format_html for HTML built in Python.
- Scope querysets to the requesting user or tenant (request.user.orders.all()) and check object permissions.
- Production settings pass python manage.py check --deploy: SECURE_* headers, HSTS, secure cookies, ALLOWED_HOSTS set.
- Keep SECRET_KEY out of the repository; rotate with SECRET_KEY_FALLBACKS.
- Use parameterised queries or the ORM's query builder; never format SQL with user input.
- Call subprocess with a list of arguments and shell=False; never pass untrusted text to a shell.
Structure
- One Django app per bounded domain area; models.py, views.py, urls.py, forms.py, admin.py, services.py/selectors.py as the project already does.
- Define a custom user model (AUTH_USER_MODEL) at project start; refer to it via settings.AUTH_USER_MODEL or get_user_model().
- Keep settings in a settings package or one module driven by environment variables; secrets from the environment or a secret store.
- Put management commands in
/management/commands/ for operational scripts instead of ad-hoc scripts. - Keep the repository's package layout; place new modules next to the code they belong with.
- Declare project metadata and tool configuration in pyproject.toml when the repository uses it.
- Keep import order standard library, third party, local, and avoid wildcard imports.
- Guard executable scripts with if name == "main" and keep logic importable.
Testing
- Use django.test.TestCase (transaction per test) or pytest-django with the db fixture, consistent with the repository.
- Use the test Client or RequestFactory for views; assertNumQueries (or django_assert_num_queries) for list views to lock in query counts.
- Build data with factories (factory_boy) or setUpTestData, not fixtures that drift from the schema.
- Test permissions and CSRF-protected flows, not only happy paths; test migrations that transform data.
- Unit-test model methods, forms and services without the HTTP layer; integration-test views, permissions and transactions through the test Client against the test database.
- Name test methods test_
(test_anonymous_user_is_redirected_to_login) and keep one behaviour per test. - Done means tested: new or changed behaviour (a view, a migration that transforms data, a permission) ships with tests in the same change.
- Write pytest tests named after the behaviour they check, in the repository's tests directory.
Review checklist
- Are querysets used in loops or templates optimised with select_related/prefetch_related?
- Does every model change include a migration, and is it safe to deploy while old code runs?
- Is input validated through forms or serializers with explicitly listed fields?
- Are querysets scoped to the current user or tenant, with permission checks on objects?
- Is CSRF protection intact and is no user content marked safe?
- Is raw SQL avoided or parameterised?
- Are multi-step writes atomic, with side effects deferred to on_commit?
- Is domain logic in models, QuerySets or service functions rather than in views or signals?
- Are settings read from the environment, with no secrets or DEBUG=True committed?
- Do tests cover views (including permission failures) and lock query counts for list views?
- Are URLs reversed by name rather than hard-coded?
- Are new indexes or constraints added for new filters and uniqueness rules?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
django-missing-migrations |
static | django makemigrations | no |
django-check |
static | django system checks | no |
djlint |
lint | djlint | no |
ruff |
lint | ruff | no |
ruff-format |
format | ruff | no |
black |
format | black | no |
mypy |
typecheck | mypy | no |
pyright |
typecheck | pyright | no |
bandit |
security | bandit | no |
pip-audit |
security | pip-audit | no |
gitleaks |
security | gitleaks | no |