Django 4.2 release notes

April 3, 2023

Welcome to Django 4.2!

These release notes cover the new features, as well as some backwards incompatible changes you’ll want to be aware of when upgrading from Django 4.1 or earlier. We’ve begun the deprecation process for some features.

See the How to upgrade Django to a newer version guide if you’re updating an existing project.

Django 4.2 is designated as a long-term support release. It will receive security updates for at least three years after its release. Support for the previous LTS, Django 3.2, will end in April 2024.

Python compatibility

Django 4.2 supports Python 3.8, 3.9, 3.10, 3.11, and 3.12 (as of 4.2.8). We highly recommend and only officially support the latest release of each series.

What’s new in Django 4.2

Psycopg 3 support

Django now supports psycopg version 3.1.8 or higher. To update your code, install the psycopg library, you don’t need to change the ENGINE as django.db.backends.postgresql supports both libraries.

Support for psycopg2 is likely to be deprecated and removed at some point in the future.

Be aware that psycopg 3 introduces some breaking changes over psycopg2. As a consequence, you may need to make some changes to account for differences from psycopg2.

Comments on columns and tables

The new Field.db_comment and Meta.db_table_comment options allow creating comments on columns and tables, respectively. For example:

  1. from django.db import models
  2. class Question(models.Model):
  3. text = models.TextField(db_comment="Poll question")
  4. pub_date = models.DateTimeField(
  5. db_comment="Date and time when the question was published",
  6. )
  7. class Meta:
  8. db_table_comment = "Poll questions"
  9. class Answer(models.Model):
  10. question = models.ForeignKey(
  11. Question,
  12. on_delete=models.CASCADE,
  13. db_comment="Reference to a question",
  14. )
  15. answer = models.TextField(db_comment="Question answer")
  16. class Meta:
  17. db_table_comment = "Question answers"

Also, the new AlterModelTableComment operation allows changing table comments defined in the Meta.db_table_comment.

Mitigation for the BREACH attack

GZipMiddleware now includes a mitigation for the BREACH attack. It will add up to 100 random bytes to gzip responses to make BREACH attacks harder. Read more about the mitigation technique in the Heal The Breach (HTB) paper.

In-memory file storage

The new django.core.files.storage.InMemoryStorage class provides a non-persistent storage useful for speeding up tests by avoiding disk access.

Custom file storages

The new STORAGES setting allows configuring multiple custom file storage backends. It also controls storage engines for managing files (the "default" key) and static files (the "staticfiles" key).

The old DEFAULT_FILE_STORAGE and STATICFILES_STORAGE settings are deprecated as of this release.

Minor features

django.contrib.admin

  • The light or dark color theme of the admin can now be toggled in the UI, as well as being set to follow the system setting.
  • The admin’s font stack now prefers system UI fonts and no longer requires downloading fonts. Additionally, CSS variables are available to more easily override the default font families.
  • The admin/delete_confirmation.html template now has some additional blocks and scripting hooks to ease customization.
  • The chosen options of filter_horizontal and filter_vertical widgets are now filterable.
  • The admin/base.html template now has a new block nav-breadcrumbs which contains the navigation landmark and the breadcrumbs block.
  • ModelAdmin.list_editable now uses atomic transactions when making edits.
  • jQuery is upgraded from version 3.6.0 to 3.6.4.

django.contrib.auth

  • The default iteration count for the PBKDF2 password hasher is increased from 390,000 to 600,000.
  • UserCreationForm now saves many-to-many form fields for a custom user model.
  • The new BaseUserCreationForm is now the recommended base class for customizing the user creation form.

django.contrib.gis

  • The GeoJSON serializer now outputs the id key for serialized features, which defaults to the primary key of objects.
  • The GDALRaster class now supports pathlib.Path.
  • The GeoIP2 class now supports .mmdb files downloaded from DB-IP.
  • The OpenLayers template widget no longer includes inline CSS (which also removes the former map_css block) to better comply with a strict Content Security Policy.
  • OpenLayersWidget is now based on OpenLayers 7.2.2 (previously 4.6.5).
  • The new isempty lookup and IsEmpty() expression allow filtering empty geometries on PostGIS.
  • The new FromWKB() and FromWKT() functions allow creating geometries from Well-known binary (WKB) and Well-known text (WKT) representations.

django.contrib.postgres

django.contrib.sitemaps

django.contrib.staticfiles

  • ManifestStaticFilesStorage now has experimental support for replacing paths to JavaScript modules in import and export statements with their hashed counterparts. If you want to try it, subclass ManifestStaticFilesStorage and set the support_js_module_import_aggregation attribute to True.
  • The new ManifestStaticFilesStorage.manifest_hash attribute provides a hash over all files in the manifest and changes whenever one of the files changes.

Database backends

Error Reporting

Forms

  • ModelForm now accepts the new Meta option formfield_callback to customize form fields.
  • modelform_factory() now respects the formfield_callback attribute of the form’s Meta.

Internationalization

  • Added support and translations for the Central Kurdish (Sorani) language.

Logging

  • The django.db.backends logger now logs transaction management queries (BEGIN, COMMIT, and ROLLBACK) at the DEBUG level.

Management Commands

  • makemessages command now supports locales with private sub-tags such as nl_NL-x-informal.
  • The new makemigrations —update option merges model changes into the latest migration and optimizes the resulting operations.

Migrations

  • Migrations now support serialization of enum.Flag objects.

Models

  • QuerySet now extensively supports filtering against Window functions with the exception of disjunctive filter lookups against window functions when performing aggregation.
  • prefetch_related() now supports Prefetch objects with sliced querysets.
  • Registering lookups on Field instances is now supported.
  • The new robust argument for on_commit() allows performing actions that can fail after a database transaction is successfully committed.
  • The new KT() expression represents the text value of a key, index, or path transform of JSONField.
  • Now now supports microsecond precision on MySQL and millisecond precision on SQLite.
  • F() expressions that output BooleanField can now be negated using ~F() (inversion operator).
  • Model now provides asynchronous versions of some methods that use the database, using an a prefix: adelete(), arefresh_from_db(), and asave().
  • Related managers now provide asynchronous versions of methods that change a set of related objects, using an a prefix: aadd(), aclear(), aremove(), and aset().
  • CharField.max_length is no longer required to be set on PostgreSQL, which supports unlimited VARCHAR columns.

Requests and Responses

Tests

  • The test —debug-sql option now formats SQL queries with sqlparse.

  • The RequestFactory, AsyncRequestFactory, Client, and AsyncClient classes now support the headers parameter, which accepts a dictionary of header names and values. This allows a more natural syntax for declaring headers.

    1. # Before:
    2. self.client.get("/home/", HTTP_ACCEPT_LANGUAGE="fr")
    3. await self.async_client.get("/home/", ACCEPT_LANGUAGE="fr")
    4. # After:
    5. self.client.get("/home/", headers={"accept-language": "fr"})
    6. await self.async_client.get("/home/", headers={"accept-language": "fr"})

Utilities

  • The new encoder parameter for django.utils.html.json_script() function allows customizing a JSON encoder class.
  • The private internal vendored copy of urllib.parse.urlsplit() now strips '\r', '\n', and '\t' (see CVE-2022-0391 and bpo-43882). This is to protect projects that may be incorrectly using the internal url_has_allowed_host_and_scheme() function, instead of using one of the documented functions for handling URL redirects. The Django functions were not affected.
  • The new django.utils.http.content_disposition_header() function returns a Content-Disposition HTTP header value as specified by RFC 6266.

Validators

  • The list of common passwords used by CommonPasswordValidator is updated to the most recent version.

Backwards incompatible changes in 4.2

Database backend API

This section describes changes that may be needed in third-party database backends.

  • DatabaseFeatures.allows_group_by_pk is removed as it only remained to accommodate a MySQL extension that has been supplanted by proper functional dependency detection in MySQL 5.7.15. Note that DatabaseFeatures.allows_group_by_selected_pks is still supported and should be enabled if your backend supports functional dependency detection in GROUP BY clauses as specified by the SQL:1999 standard.
  • inspectdb now uses display_size from DatabaseIntrospection.get_table_description() rather than internal_size for CharField.

Dropped support for MariaDB 10.3

Upstream support for MariaDB 10.3 ends in May 2023. Django 4.2 supports MariaDB 10.4 and higher.

Dropped support for MySQL 5.7

Upstream support for MySQL 5.7 ends in October 2023. Django 4.2 supports MySQL 8 and higher.

Dropped support for PostgreSQL 11

Upstream support for PostgreSQL 11 ends in November 2023. Django 4.2 supports PostgreSQL 12 and higher.

Setting update_fields in Model.save() may now be required

In order to avoid updating unnecessary columns, QuerySet.update_or_create() now passes update_fields to the Model.save() calls. As a consequence, any fields modified in the custom save() methods should be added to the update_fields keyword argument before calling super(). See Overriding predefined model methods for more details.

Dropped support for raw aggregations on MySQL

MySQL 8+ allows functional dependencies on GROUP BY columns, so the pre-Django 4.2 workaround of grouping by primary keys of the main table is removed. As a consequence, using RawSQL() aggregations is no longer supported on MySQL as there is no way to determine if such aggregations are needed or valid in the GROUP BY clause. Use Aggregation functions instead.

Miscellaneous

  • The undocumented django.http.multipartparser.parse_header() function is removed. Use django.utils.http.parse_header_parameters() instead.
  • {% blocktranslate asvar … %} result is now marked as safe for (HTML) output purposes.
  • The autofocus HTML attribute in the admin search box is removed as it can be confusing for screen readers.
  • The makemigrations —check option no longer creates missing migration files.
  • The alias argument for Expression.get_group_by_cols() is removed.
  • The minimum supported version of sqlparse is increased from 0.2.2 to 0.3.1.
  • The undocumented negated parameter of the Exists expression is removed.
  • The is_summary argument of the undocumented Query.add_annotation() method is removed.
  • The minimum supported version of SQLite is increased from 3.9.0 to 3.21.0.
  • The minimum supported version of asgiref is increased from 3.5.2 to 3.6.0.
  • UserCreationForm now rejects usernames that differ only in case. If you need the previous behavior, use BaseUserCreationForm instead.
  • The minimum supported version of mysqlclient is increased from 1.4.0 to 1.4.3.
  • The minimum supported version of argon2-cffi is increased from 19.1.0 to 19.2.0.
  • The minimum supported version of Pillow is increased from 6.2.0 to 6.2.1.
  • The minimum supported version of jinja2 is increased from 2.9.2 to 2.11.0.
  • The minimum supported version of redis-py is increased from 3.0.0 to 3.4.0.
  • Manually instantiated WSGIRequest objects must be provided a file-like object for wsgi.input. Previously, Django was more lax than the expected behavior as specified by the WSGI specification.
  • Support for PROJ < 5 is removed.
  • EmailBackend now verifies a hostname and certificates. If you need the previous behavior that is less restrictive and not recommended, subclass EmailBackend and override the ssl_context property.

Features deprecated in 4.2

index_together option is deprecated in favor of indexes

The Meta.index_together option is deprecated in favor of the indexes option.

Migrating existing index_together should be handled as a migration. For example:

  1. class Author(models.Model):
  2. rank = models.IntegerField()
  3. name = models.CharField(max_length=30)
  4. class Meta:
  5. index_together = [["rank", "name"]]

Should become:

  1. class Author(models.Model):
  2. rank = models.IntegerField()
  3. name = models.CharField(max_length=30)
  4. class Meta:
  5. indexes = [models.Index(fields=["rank", "name"])]

Running the makemigrations command will generate a migration containing a RenameIndex operation which will rename the existing index. Next, consider squashing migrations to remove index_together from historical migrations.

The AlterIndexTogether migration operation is now officially supported only for pre-Django 4.2 migration files. For backward compatibility reasons, it’s still part of the public API, and there’s no plan to deprecate or remove it, but it should not be used for new migrations. Use AddIndex and RemoveIndex operations instead.

Passing encoded JSON string literals to JSONField is deprecated

JSONField and its associated lookups and aggregates used to allow passing JSON encoded string literals which caused ambiguity on whether string literals were already encoded from database backend’s perspective.

During the deprecation period string literals will be attempted to be JSON decoded and a warning will be emitted on success that points at passing non-encoded forms instead.

Code that used to pass JSON encoded string literals:

  1. Document.objects.bulk_create(
  2. Document(data=Value("null")),
  3. Document(data=Value("[]")),
  4. Document(data=Value('"foo-bar"')),
  5. )
  6. Document.objects.annotate(
  7. JSONBAgg("field", default=Value("[]")),
  8. )

Should become:

  1. Document.objects.bulk_create(
  2. Document(data=Value(None, JSONField())),
  3. Document(data=[]),
  4. Document(data="foo-bar"),
  5. )
  6. Document.objects.annotate(
  7. JSONBAgg("field", default=[]),
  8. )

From Django 5.1+ string literals will be implicitly interpreted as JSON string literals.

Miscellaneous

  • The BaseUserManager.make_random_password() method is deprecated. See recipes and best practices for using Python’s secrets module to generate passwords.

  • The length_is template filter is deprecated in favor of length and the == operator within an {% if %} tag. For example

    1. {% if value|length == 4 %}…{% endif %}
    2. {% if value|length == 4 %}True{% else %}False{% endif %}

    instead of:

    1. {% if value|length_is:4 %}…{% endif %}
    2. {{ value|length_is:4 }}
  • django.contrib.auth.hashers.SHA1PasswordHasher, django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher, and django.contrib.auth.hashers.UnsaltedMD5PasswordHasher are deprecated.

  • django.contrib.postgres.fields.CICharField is deprecated in favor of CharField(db_collation="…") with a case-insensitive non-deterministic collation.

  • django.contrib.postgres.fields.CIEmailField is deprecated in favor of EmailField(db_collation="…") with a case-insensitive non-deterministic collation.

  • django.contrib.postgres.fields.CITextField is deprecated in favor of TextField(db_collation="…") with a case-insensitive non-deterministic collation.

  • django.contrib.postgres.fields.CIText mixin is deprecated.

  • The map_height and map_width attributes of BaseGeometryWidget are deprecated, use CSS to size map widgets instead.

  • SimpleTestCase.assertFormsetError() is deprecated in favor of assertFormSetError().

  • TransactionTestCase.assertQuerysetEqual() is deprecated in favor of assertQuerySetEqual().

  • Passing positional arguments to Signer and TimestampSigner is deprecated in favor of keyword-only arguments.

  • The DEFAULT_FILE_STORAGE setting is deprecated in favor of STORAGES["default"].

  • The STATICFILES_STORAGE setting is deprecated in favor of STORAGES["staticfiles"].

  • The django.core.files.storage.get_storage_class() function is deprecated.