Skip to content
Community content. Review instructions before giving them to an AI agent — treat modules like open-source code.

Python Django Conventions

Django project conventions: fat models thin views, queryset optimization with select_related and prefetch_related, migrations discipline, forms/serializers for validation, per-environment settings, and app boundaries.

Mby @markdownersPublished August 21, 2026 · ~4 min read

0 downloads · Used by 0 stacks

Push business logic into models and managers, keep views as thin coordinators that call into that logic and render a response — a view with more than a few lines of domain logic is a sign that logic belongs somewhere else.

Fat models, thin views

  • Put domain logic (state transitions, computed properties, validation rules that always apply) on the model or a custom manager/queryset method, not in the view function or class — a Order.mark_paid() method is reusable from a view, a management command, and a test; logic inlined in a view is not.
  • Keep views responsible for HTTP concerns only: parsing the request, calling the domain method, choosing the response — never let a view directly manipulate several related models' fields inline when a model method could encapsulate that transition.
  • Extract logic shared by two or more views into a service function or model method before a third view needs it, not after.

Queryset optimization

  • Use select_related for forward foreign-key/one-to-one relations and prefetch_related for reverse foreign-key/many-to-many relations whenever a template or serializer will access them, to avoid the N+1 query pattern of one query per row.
  • Never access a related object in a loop over a queryset without having prefetched it first — profile with Django's query count (debug toolbar, assertNumQueries in tests) rather than assuming the ORM is doing the right thing.
  • Prefer .values()/.values_list() or .only()/.defer() when a view genuinely needs a narrow slice of columns from a wide table, but don't reach for them by default — the readability cost of partial objects isn't worth it until a query is measurably expensive.
  • Use .exists() to check presence and .count() to count — never fetch a full queryset just to check len() or truthiness.

Migrations discipline

  • Generate migrations with makemigrations immediately after a model change and commit them in the same change as the model edit — a model and its migration must never drift apart in version control.
  • Never hand-edit a migration's schema operations after it has been applied anywhere outside your own unpushed branch; write a new migration instead.
  • Keep data migrations (RunPython) separate from schema migrations when both are needed for the same change, and always provide a reverse function (or RunPython.noop deliberately) rather than leaving a migration irreversible by accident.
  • Run migrations against a staging/copy of production data before applying to production for any migration that touches a large table or changes a column's nullability/type.

Forms and serializers for validation

  • Validate all external input (form POST, API payload) through a Form/ModelForm or DRF Serializer, never by reading request.POST/request.data fields directly in a view and trusting their shape or type.
  • Put field-level validation (format, range) on the form/serializer field, and cross-field or business-rule validation in clean()/validate() — don't scatter validation logic between the view and the model's save().
  • Reuse the same serializer/form for both the API and any internal callers (admin actions, management commands) that create or update the same model, so validation rules only exist in one place.

Settings per environment

  • Split settings into a shared base plus per-environment overrides (local, staging, production) — never branch on if DEBUG scattered through arbitrary settings; keep environment-specific values (database, allowed hosts, email backend) in their own file or env-var-driven block.
  • Load secrets (SECRET_KEY, database passwords, API keys) from environment variables, never hard-coded in a settings file that's committed to version control, including for "just local dev" — use a documented .env.example instead.
  • Set DEBUG = False and a real ALLOWED_HOSTS in every environment that isn't a developer's own machine; a DEBUG = True misconfiguration in production leaks stack traces and settings to any visitor who hits an error.

App boundaries

  • Design each Django app around one cohesive domain concept (e.g., billing, accounts), not around a technical layer (models, views) or a single feature ticket — an app should be something you could plausibly reuse or remove as a unit.
  • Avoid circular imports between apps by keeping cross-app references to IDs/foreign keys and importing the other app's public functions, not its internals; if two apps need to reach deep into each other constantly, they're probably one app.
  • Keep an app's models.py as the single source of truth for that app's schema — don't define overlapping models for the same concept in two different apps.
Badge

Link back to this module from your own README.

Get it on Markdowners
[![Get it on Markdowners](https://markdowners.com/mdstack-badge.svg)](https://markdowners.com/m/markdowners/python-django-conventions)

Comments (0)

Sign in to comment. Sign in

No comments yet. Be the first to add one.

Discussions about this module

No discussions about this module yet.

Start a discussion