Engineering Blog

Django 6.1 Preview: Key Features & Architectural Changes

Django 6.1 Preview: Key Features & Architectural Changes

Django 6.1 Preview

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 same QuerySet. This acts similarly to an automatic post-query prefetch_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 a FieldFetchBlocked exception 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:

  1. Fetches all books.
  2. 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_CASCADE
  • DB_SET_NULL
  • DB_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 next URL for already authenticated users where possible, rather than always routing them to the admin index.
  • The admin FilteredSelectMultiple widget now preserves named group choices using HTML <optgroup> elements.
  • When ModelAdmin.list_select_related defaults to False, the change list no longer selects all related foreign keys. Instead, it selects only the foreign key fields explicitly declared in ModelAdmin.list_display. This should yield significant query speedups for models with dense foreign-key relationships.
  • A delete_confirmation_max_display option has been added to limit the number of objects rendered on the admin delete confirmation page (defaults to None for 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_display table now renders boolean icons for boolean fields on related models.
  • The @action decorator supports a location argument to control whether an admin action is accessible from the change list, the change form, or both.
  • The @action decorator supports a description_plural argument 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,000 to 1,500,000.
  • Renaming a model via migrations now automatically updates Permission.name and Permission.codename.
  • A Permission.user_perm_str property has been introduced, returning the correct permission string format suitable for passing to User.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 isempty lookup and IsEmpty() database function.
  • PostGIS and SpatiaLite support the num_dimensions lookup and NumDimensions() function to filter geometries by their dimension count.
  • The admin map widget OpenLayersWidget has been upgraded from OpenLayers 7.2.2 to 10.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

  • inspectdb can now introspect HStoreField columns, provided psycopg 3.2+ is installed and django.contrib.postgres is included in INSTALLED_APPS.
  • ExclusionConstraint now 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

  • SessionBase now 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_attr template tag has been added to render the CSP nonce attribute on <script> and <link> elements or Media asset outputs when the csp() context processor is configured.
  • A new security.W027 system check raises warnings if ContentSecurityPolicyMiddleware is active and references CSP.NONCE while the csp() 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 the csp() 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 Stylesheet asset 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_LABEL constant 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 setting USE_BLANK_CHOICE_DASH).
  • FilePathField gains a set_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_request attribute preserves the HTTP request method and body upon redirect, returning 307/308 status codes instead of 302/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=True on ArgumentParser by default, yielding suggestions for mistyped subcommands or choice arguments.
  • The loaddata command now fires the m2m_changed signal with raw=True during 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 after values() and values_list().
  • A new JSONNull expression explicitly represents the JSON scalar null in DB queries, making it easier to query or persist JSON nulls distinctly from SQL NULL.
  • DecimalField no longer strictly requires max_digits and decimal_places parameters on Oracle, PostgreSQL, and SQLite.
  • Oracle 21c+ supports negative array indexing on JSONField.
  • Native UUID4 and UUID7 database functions have been added.
  • Oracle 23ai/26ai (23.7+) GeneratedField supports stored columns (db_persist=True).
  • The m2m_changed signal accepts a raw parameter.
  • SQLite supports distinct=True inside StringAgg when utilizing the default comma delimiter.
  • A new QuerySet.totally_ordered boolean 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.
  • BinaryField strictly validates Base64 inputs, raising ValidationError on 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.
  • HttpResponseRedirect subclasses and the redirect() shortcut accept a max_length parameter 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-primary is specified, falling back to serializing the primary key.
  • The XML deserializer raises SuspiciousOperation when 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 **kwargs that are passed through to the backend’s task_class.
  • Task and TaskResult instances 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() and assertNotContains() can be called multiple times against the same StreamingHttpResponse without 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.

Join the Investigation

Get the latest updates on my projects and indie hacking journey directly in your inbox.

No spam. Unsubscribe anytime.