Django 6.1 Preview: Key Features & Architectural Changes
Django 6.1 Preview: Key Features & Architectural Changes

This post provides a comprehensive summary of the notable changes and architectural advancements introduced in the Django 6.1 preview, paired with speculative insights on “why they were introduced and where they are best suited.”
1. Model Field Fetch Mode
Django model fields can now customize their lazy-loading behavior via fetch mode. This grants developers control over how Django retrieves missing attributes when they are accessed.
Django provides three fetch modes:
FETCH_ONE: Standard lazy loading (default). This is identical to traditional Django behavior.FETCH_PEERS: Fetches values for the missing field across all instances loaded by the sameQuerySet. This acts similarly to an automatic post-queryprefetch_related(). It can resolve the majority of “N+1 query problems” by condensing them into just two database calls without requiring manual prefetch lists.RAISE: Raises aFieldFetchBlockedexception when an un-fetched field is accessed. This is highly useful for preventing unexpected queries in performance-sensitive sections.
You can specify the fetch mode on model instances retrieved from a query using the new QuerySet.fetch_mode() method:
from django.db import models
books = Book.objects.fetch_mode(models.FETCH_PEERS)
for book in books:
print(book.author.name)
In the loop above, accessing book.author triggers only two queries in total thanks to FETCH_PEERS:
- Fetches all books.
- Fetches the related authors.
Probably in My View
This feature seems to have been introduced to alleviate the notorious “accidental N+1 queries” problem in Django ORM. While select_related() and prefetch_related() are powerful, they require developers to anticipate relations in advance. In real-world systems, fields are often accessed downstream in serializers, templates, or admin views, where predictions might fail.
FETCH_PEERS could be highly suitable for scenarios like lists, API index responses, admin change lists, or CSV exports where you iterate over objects of the same queryset. It might serve as a convenient compromise for developers seeking performance optimizations without manually auditing every single relation.
RAISE, on the other hand, is likely targeted at more critical execution paths—such as payment processing, batch reports, or API hot paths—where query counts must be strictly predictable. It could act as a useful guardrail to prevent unexpected database access in production and enforce strict query policies in test suites.
2. Database-level Delete Constraints in ForeignKey.on_delete
ForeignKey.on_delete now supports native database-level deletion options.
The new options are:
DB_CASCADEDB_SET_NULLDB_SET_DEFAULT
These options handle deletion logic directly within the database using SQL ON DELETE clauses, rather than at the Django Python application level. This is more efficient since Django does not need to load the child objects into memory to cascade the deletion.
However, there is a crucial difference: DB_CASCADE does not fire Django’s pre_delete and post_delete signals. If your application relies on these signals, do not switch to this option blindly.
Probably in My View
This enhancement appears to be aimed at optimizing bulk delete performance and reinforcing database-level integrity constraints. Django’s traditional python-level CASCADE requires the ORM to fetch related objects into memory, which works nicely with signals but scales poorly with millions of rows.
DB_CASCADE and its counterparts are probably best suited for relationships where no application-level side effects are needed on deletion. It might be ideal for logs, transient data, associative join tables, or metadata that must simply vanish with the parent record.
Conversely, if you rely on signals to delete physical files, make API calls, create audit logs, or rebuild search indexes, this native feature might not be suitable. It would probably be safer to stick to traditional Python-level cascade behaviors where business logic side-effects are paramount.
3. Mailers
A new MAILERS setting has been introduced, allowing multiple email backends to be configured with distinct options. The structure mirrors existing alias-based settings like CACHES, DATABASES, STORAGES, and TASKS.
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {"host": "smtp.example.com", "use_tls": True},
},
"marketing": {
"BACKEND": "example.third.party.EmailBackend",
"OPTIONS": {"region": "africa-1"},
},
}
You can choose a specific mailer in email-sending functions using the new using argument, or retrieve an email backend instance using mail.mailers[alias].
MAILERS is not enabled by default for existing projects yet. It is scheduled to replace the legacy EMAIL_BACKEND and related EMAIL_* settings in Django 7.0. While the old configuration format still functions in Django 6.1, it triggers a deprecation warning, making it advisable to transition to MAILERS ahead of 7.0.
Probably in My View
This change was likely prompted by the fact that modern web services have moved away from a single SMTP server. Today, applications routinely distribute transactional emails, newsletters, admin alerts, and password resets across different email service providers.
The legacy configuration model was clean for single backends but clumsy for multi-provider environments. MAILERS seems to organize this into an alias-based registry similar to how databases or caches are structured.
It might be particularly beneficial for SaaS platforms where you want to split traffic: using a high-deliverability provider for login emails (default), a cheaper service for newsletters (marketing), and an internal relay for alerts (alerts). Projects needing segregated rate limits, regions, or credentials will likely find this structural change very helpful.
4. django.contrib.admin Improvements
- The admin login view now redirects to the
nextURL for already authenticated users where possible, rather than always routing them to the admin index. - The admin
FilteredSelectMultiplewidget now preserves named group choices using HTML<optgroup>elements. - When
ModelAdmin.list_select_relateddefaults toFalse, the change list no longer selects all related foreign keys. Instead, it selects only the foreign key fields explicitly declared inModelAdmin.list_display. This should yield significant query speedups for models with dense foreign-key relationships. - A
delete_confirmation_max_displayoption has been added to limit the number of objects rendered on the admin delete confirmation page (defaults toNonefor no limit). - Admin change form layouts have been adjusted for web accessibility. Form fields are now positioned below labels, and help texts as well as validation errors are placed before inputs (checkboxes maintain their traditional layout).
- The
list_displaytable now renders boolean icons for boolean fields on related models. - The
@actiondecorator supports alocationargument to control whether an admin action is accessible from the change list, the change form, or both. - The
@actiondecorator supports adescription_pluralargument to specify a pluralized label for actions in the change list.
Probably in My View
While the admin panel is Django’s killer feature, its legacy UI structure and bulk data performance issues have sometimes been pain points. These adjustments seem to focus on enhancing performance, accessibility, and action ergonomics.
The change to list_select_related query behavior appears to target database footprint optimization for models with numerous foreign keys. Fetching everything by default is convenient for small prototypes but can lead to bloated queries in complex domains. This optimization will likely benefit high-traffic tables like orders, campaigns, or logs.
delete_confirmation_max_display might help mitigate browser lag or memory crashes when administrators attempt to delete thousands of rows at once.
The accessibility upgrades could prove highly valuable for enterprise tools and public services, where keyboard navigation and clear labeling are compliance requirements.
The new location option in the @action decorator seems to reflect a desire to let administrators invoke actions directly from individual change forms, streamlining single-object workflows like processing a refund or suspending a user.
5. django.contrib.auth Improvements
- The PBKDF2 password hasher default iterations have been raised from
1,200,000to1,500,000. - Renaming a model via migrations now automatically updates
Permission.nameandPermission.codename. - A
Permission.user_perm_strproperty has been introduced, returning the correct permission string format suitable for passing toUser.has_perm().
Probably in My View
The increase in PBKDF2 iterations appears to be a routine security baseline upgrade matching modern hardware capabilities, driving up the computing cost for brute-force attacks. Standard projects should probably stick to this new default.
The automatic permission updates on model renames seem intended to address a long-standing developer annoyance where permission names stayed outdated after database refactoring.
Permission.user_perm_str will likely help prevent syntax errors or typos when manually concatenating permission strings, making permission verification code cleaner and safer.
6. django.contrib.gis Improvements
- SpatiaLite now supports the
isemptylookup andIsEmpty()database function. - PostGIS and SpatiaLite support the
num_dimensionslookup andNumDimensions()function to filter geometries by their dimension count. - The admin map widget
OpenLayersWidgethas been upgraded from OpenLayers7.2.2to10.9.0.
Probably in My View
Although PostGIS is the gold standard for spatial backends, SpatiaLite remains important for local development and lightweight deployments. Expanding SpatiaLite support seems aimed at closing the feature gap and facilitating seamless environment transitions.
num_dimensions lookup might be particularly handy in mixed 2D/3D datasets common in geography, logistics, and real estate, where structural validation is critical.
The OpenLayers upgrade is likely a maintenance patch to bring modern map interactions and security compliance to the default admin map widget.
7. django.contrib.postgres Improvements
inspectdbcan now introspectHStoreFieldcolumns, providedpsycopg 3.2+is installed anddjango.contrib.postgresis included inINSTALLED_APPS.ExclusionConstraintnow supports the Hash index type.
Probably in My View
Introspecting HStoreField via inspectdb seems designed to streamline legacy database reverse-engineering, letting developers cleanly map existing Postgres tables into Django models.
Adding Hash index support for ExclusionConstraint appears to be a move to expose more of Postgres’s robust integrity checks to the ORM, allowing developers to manage advanced overlaps (like booking schedules or spatial conflicts) directly at the database level.
8. django.contrib.sessions Improvements
SessionBasenow supports boolean evaluation via__bool__().
Probably in My View
This looks like a syntactic sugar improvement to write cleaner conditional statements. The new __bool__() method does not merely check whether the session object itself exists. Instead, it evaluates whether the underlying session dictionary contains data. In other words, even if request.session has already been created, bool(request.session) returns False when there are no stored key-value pairs.
That means if request.session: should be read as “does this session contain data?” rather than “does a session object exist?” It is a natural fit for checking whether the session is empty, while code that depends on a specific session value should still check that key or value explicitly.
9. CSP Improvements
- A new
csp_nonce_attrtemplate tag has been added to render the CSP nonce attribute on<script>and<link>elements orMediaasset outputs when thecsp()context processor is configured. - A new
security.W027system check raises warnings ifContentSecurityPolicyMiddlewareis active and referencesCSP.NONCEwhile thecsp()context processor is missing from template settings. - CSP nonces are now automatically attached to
<script>,<style>, and<link>tags in the default Django admin and built-in templates when thecsp()context processor is set up.
Probably in My View
Implementing CSP nonces is excellent for security, but manually decorating every single script tag and form asset is notoriously tedious. These additions seem focused on enabling strict CSP policies without breaking the default Django admin interface.
This will likely benefit fintech, healthcare, and enterprise B2B apps where mitigating XSS attacks is a high priority.
The security.W027 warning will probably save developers from configuration blunders early on, ensuring nonces aren’t quietly ignored in production.
10. Forms Improvements
- A new
Stylesheetasset object has been introduced, allowing custom HTML attributes (like integrity hashes, preloads, or media queries) to be attached to stylesheet link elements in form media. - The
django.db.models.fields.BLANK_CHOICE_LABELconstant has been added, defining the default blank choice label in form dropdowns in a more accessible and translatable way. (Projects wanting to revert to the old dashes format can utilize the transition settingUSE_BLANK_CHOICE_DASH). FilePathFieldgains aset_choices()method to scan the target path directory again. Invoking this inside a form’s__init__()updates choices dynamically on every request.
Probably in My View
The Stylesheet class seems to address modern frontend needs like adding SRI hashes, preloads, or media queries to styles attached to form widgets.
BLANK_CHOICE_LABEL appears to resolve an accessibility and localization issue, as the legacy dashed string (”---------”) was not screen-reader friendly.
FilePathField.set_choices() will likely be helpful for internal tools where directory contents change dynamically at runtime (such as scanning recently uploaded CSV files or templates). Developers should probably still exercise caution regarding path disclosure when using it on public forms.
11. Generic Views Improvements
- The
RedirectView.preserve_requestattribute preserves the HTTP request method and body upon redirect, returning307/308status codes instead of302/301.
Probably in My View
Standard 302 redirects typically strip HTTP methods and request bodies, turning POSTs into GETs. While this is expected in standard web navigation, it breaks APIs and webhooks. preserve_request seems to fill this gap by utilizing 307/308 status codes.
It might be ideal for API gateway views, routing deprecated endpoints, or shifting webhook destinations without losing payload data. For typical Post-Redirect-Get user flows, the traditional 302 is likely still the correct choice.
12. Management Commands Improvements
- For Python 3.14+, management commands now enable
suggest_on_error=TrueonArgumentParserby default, yielding suggestions for mistyped subcommands or choice arguments. - The
loaddatacommand now fires them2m_changedsignal withraw=Trueduring fixture loading.
Probably in My View
The suggest_on_error flag appears to leverage Python 3.14’s argparse improvement, making CLI operations less frustrating for developers by suggesting corrections for mistyped subcommands.
Using raw=True for m2m_changed during loaddata is likely designed to suppress runtime side effects (like rebuilding cache or firing external webhooks) when loading initial fixtures, which could prevent database pollution.
13. Models Improvements
QuerySet.in_bulk()can now be chained aftervalues()andvalues_list().- A new
JSONNullexpression explicitly represents the JSON scalarnullin DB queries, making it easier to query or persist JSON nulls distinctly from SQLNULL. DecimalFieldno longer strictly requiresmax_digitsanddecimal_placesparameters on Oracle, PostgreSQL, and SQLite.- Oracle 21c+ supports negative array indexing on
JSONField. - Native
UUID4andUUID7database functions have been added. - Oracle 23ai/26ai (23.7+)
GeneratedFieldsupports stored columns (db_persist=True). - The
m2m_changedsignal accepts arawparameter. - SQLite supports
distinct=TrueinsideStringAggwhen utilizing the default comma delimiter. - A new
QuerySet.totally_orderedboolean attribute determines if query ordering is deterministic. - Bitwise aggregates (
BitAnd,BitOr,BitXor) are now standard aggregates in Django models, promoted from the Postgres-specific contrib module. BinaryFieldstrictly validates Base64 inputs, raisingValidationErroron malformed values rather than silently accepting them.
Probably in My View
This bundle seems to focus on narrowing the gap between database backends while refining SQL edge cases in the ORM.
JSONNull is a particularly welcome change. Distinguishing SQL NULL from JSON null is historically tricky in Python because both map to None. This addition will likely make querying JSONFields significantly less error-prone.
UUID7 database-level generation is also a major win. Because UUID7 contains timestamp info, it maintains indexing locality, unlike the chaotic UUID4. It could be highly beneficial for write-heavy tables like logs, orders, or event feeds.
QuerySet.totally_ordered should be useful for ensuring pagination safety, as non-deterministic ordering causes duplicate or missing items during page transitions—especially in cursor-based or infinite scroll pagination.
Strict Base64 validation in BinaryField likely aims to prevent database corruption by rejecting invalid payloads early, which is essential for security-sensitive binaries like tokens or keys.
14. Requests and Responses Improvements
- The multipart parser class can now be customized by overriding
HttpRequest.multipart_parser_class. HttpResponseRedirectsubclasses and theredirect()shortcut accept amax_lengthparameter to override the default URL length limit.
Probably in My View
Overriding multipart_parser_class seems targeted at advanced file-handling setups, such as custom streaming filters, virus scanner integration, or handling massive uploads efficiently.
Allowing custom max_length in redirects is likely a workaround for complex SSO, SAML, or OAuth workflows where token-loaded URLs exceed standard browser/server limits. Developers should probably still avoid excessively long URLs to maintain proxy compatibility.
15. Serialization Improvements
- Model subclasses defining
natural_key()can return an empty tuple()to opt out of natural key serialization when--natural-primaryis specified, falling back to serializing the primary key. - The XML deserializer raises
SuspiciousOperationwhen encountering unexpected nested XML elements.
Probably in My View
Opting out of natural keys will likely give developers finer control when exporting test fixtures, especially in complex model hierarchies where natural keys aren’t desirable for specific subclasses.
The XML deserializer exception is clearly a security hardening measure to prevent XML Entity expansion (XXE) or resource exhaustion attacks when importing untrusted XML documents.
16. Tasks Improvements
- The
task()decorator now accepts arbitrary**kwargsthat are passed through to the backend’stask_class. TaskandTaskResultinstances now support pickling and unpickling.
Probably in My View
These changes seem designed to make Django’s built-in background task system more extensible, allowing custom task queues to consume custom backend parameters directly from decorators.
Pickle support likely serves as a foundation for passing task results across worker processes or caching them. However, since pickling is prone to RCE vulnerabilities, developers should probably restrict this to highly secure, internal backends.
17. Tests Improvements
assertContains()andassertNotContains()can be called multiple times against the sameStreamingHttpResponsewithout raising exceptions due to consumed generator contents.
Probably in My View
Testing StreamingHttpResponse was historically annoying because generators are exhausted after the first assertion. This update likely aims to make testing streaming APIs, CSV exports, or SSE responses much cleaner by allowing multiple text inclusions to be validated sequentially.
18. Utilities Improvements
parse_duration()now supports ISO 8601 week formatting, e.g.,PnW.
Probably in My View
This simple change appears to be a compliance enhancement for ISO 8601 durations, making it easier to parse weekly schedule intervals (P2W etc.) from third-party APIs without manual parsing logic.