Open-Source Wikis

/

Django

/

How to contribute

/

Patterns and conventions

django/django

Patterns and conventions

Recurring idioms and rules of thumb in the Django codebase. The official prose lives in docs/internals/contributing/writing-code/coding-style.txt; this page is the engineer-oriented summary.

Coding style

  • Black-formatted, target Python 3.12 (pyproject.toml [tool.black]).
  • isort, profile = black, first-party = django (pyproject.toml [tool.isort]).
  • flake8 for the rest (.flake8). Maximum line length is 88 (black's default).
  • pre-commit runs black, isort, flake8, pyupgrade, and a few custom checks before each commit (.pre-commit-config.yaml).
  • The pyupgrade step targets --py312-plus. Don't add __future__ imports.

Public vs private API

A name is public if it's documented in docs/. A name is private if it isn't, regardless of whether it starts with an underscore. Most module-level names that aren't underscored are still considered private until they're documented.

  • django.X and django.Y.Z are public when listed in docs/ref/.
  • django.contrib.<app> public surface is documented under docs/ref/contrib/<app>.txt.
  • Backend internals (django/db/backends/*) are private. Third-party backends rely on them in practice, so changes go through deprecation cycles even though they're nominally private.

Deprecation cycles

Django ships changes that affect public API across two release cycles:

  1. Release N: deprecation warning. The old behaviour still works.
  2. Release N+1: documented as deprecated; warning level escalated.
  3. Release N+2: removed.

django/utils/deprecation.py provides RemovedInDjangoXXWarning classes. Each release branch defines its own:

  • RemovedInDjango60Warning
  • RemovedInDjango70Warning

When deprecating something, add a warning with the appropriate class, document the deprecation under docs/releases/<next>.txt "Deprecated features" section, and add a "deprecated since" comment in the docstring. The actual removal happens in a later cycle.

Lazy patterns

Django avoids importing things at module load when possible. Common patterns:

  • SimpleLazyObject / LazyObject (django/utils/functional.py) — defer instantiation until first access. Used for request.user, settings, the URL resolver.
  • @cached_property (django/utils/functional.py) — memoise the first call. Used heavily on model meta and on bound fields.
  • functools.lru_cache — used inside utility modules where the input domain is small.
  • String references for app/model lookups"auth.User" instead of importing the model class. Resolved by the app registry at runtime.

Async-safe wrappers

Anywhere the framework exposes a synchronous API that may be called from async code, there's a paired a prefix:

  • Model.save() / Model.asave()
  • QuerySet.get() / QuerySet.aget()
  • Manager.create() / Manager.acreate()
  • Client.get() / AsyncClient.get()

The a-prefixed methods use asgiref.sync.sync_to_async under the hood. New code that adds sync API should also add the async sibling.

Checks framework

Anything that can fail at startup with a clear configuration error should register a system check, not raise from the import. Checks live in django/core/checks/ and in per-app checks.py modules. Errors are tagged with stable IDs (auth.E001, models.W042, etc.) so users can silence them in SILENCED_SYSTEM_CHECKS.

Error messages

Django's user-facing error messages have a consistent style:

  • Active voice, present tense: "Cannot add a unique constraint", not "A unique constraint cannot be added".
  • Include the offending value, but never include sensitive data: f"Invalid value: {value!r}" is okay; including a password is not.
  • Use gettext_lazy for messages displayed to end users (e.g., form validation errors, admin labels). Plain f-strings are fine for developer-facing exceptions.

Lazy translation

User-facing strings are wrapped in gettext_lazy (often imported as _):

from django.utils.translation import gettext_lazy as _

class Meta:
    verbose_name = _("Author")

The lazy form delays the translation lookup until the string is rendered, so it works inside class attributes that are evaluated at import time.

Database backend abstractions

Avoid SQL string concatenation in cross-backend code. Use:

  • connection.ops.<method> for backend-specific SQL fragments.
  • connection.features.<flag> for feature flags (e.g., supports_json_field, supports_expression_indexes).
  • The Q/F/OuterRef/Subquery expression hierarchy for query construction.
  • The schema editor (django/db/backends/base/schema.py and per-backend overrides) for DDL.

If you must drop to raw SQL (rare), do it through connection.cursor() and parameterise — never f-string a value into a query.

Migration philosophy

  • Auto-detect, don't auto-mutate. makemigrations produces a migration file and stops. The user reviews and runs migrate separately.
  • Backwards-compatible by default. Adding columns is fine; removing or renaming columns goes through a deprecation cycle in user data terms.
  • RunPython is for data, RunSQL is for backend-specific DDL, schema operations are everything else. Don't mix data migrations and schema migrations in the same file.

Imports

Imports follow isort profile black, with these section orderings:

  1. Standard library
  2. Third party
  3. First-party django.*
  4. Local

Inside django/, prefer absolute imports over relative imports (from django.db.models import Q, not from ..db.models import Q). Within a single subpackage, relative imports are sometimes used for tightly-coupled internal modules.

Tests as specs

Most public behaviour is documented twice: in docs/ref/ for users, and as a test in tests/ for the implementation. When the docs and the tests disagree, the tests are the source of truth — file a docs ticket, don't change the implementation.

Don't break the matrix

Django supports multiple Python versions, multiple databases, and both sync and async paths simultaneously. Before claiming a feature is done:

  • Run the suite on SQLite (default).
  • Run the suite on at least one of PostgreSQL/MySQL.
  • Run any added tests in async mode if the feature has async coverage.
  • Confirm tox -e docs builds without warnings.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Django wiki | Factory