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
pyupgradestep 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.Xanddjango.Y.Zare public when listed indocs/ref/.django.contrib.<app>public surface is documented underdocs/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:
- Release N: deprecation warning. The old behaviour still works.
- Release N+1: documented as deprecated; warning level escalated.
- Release N+2: removed.
django/utils/deprecation.py provides RemovedInDjangoXXWarning classes. Each release branch defines its own:
RemovedInDjango60WarningRemovedInDjango70Warning- …
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 forrequest.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_lazyfor 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/Subqueryexpression hierarchy for query construction. - The schema editor (
django/db/backends/base/schema.pyand 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.
makemigrationsproduces a migration file and stops. The user reviews and runsmigrateseparately. - Backwards-compatible by default. Adding columns is fine; removing or renaming columns goes through a deprecation cycle in user data terms.
RunPythonis for data,RunSQLis 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:
- Standard library
- Third party
- First-party
django.* - 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 docsbuilds without warnings.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.