What’s new in Django 4.0

In December 2021, the Django team released Django v4. In this article, we would try to cover all the major changes, some minor changes as well as some backward-incompatible changes you’ll want to be aware of when upgrading from Django 3.2 or earlier.

Python Compatibility

Django 4.0 supports Python 3.8, 3.9, and 3.10. The Django 3.2.x series is the last to support Python 3.6 and 3.7.

Head on to your shell and type in 

python -v                      

and make sure you have a Python version greater than 3.7

What’s new in Django 4.0


Key Highlights

The new RedisCache backend provides built-in support for caching with Redis.

To ease customization of Forms, Formsets, and ErrorList they are now rendered using the template engine.

The Python standard library’s zoneinfo is now the default timezone implementation in Django.


Major Changes

Time zone from the standard library

Framework was originally designed for the Processing of time zones basically on pytz. Django 3.2 allowed the use of non-pytz time zones. Django 4.0 makes zoneinfo the default implementation. Support for pytz is now deprecated and will be removed in Django 5.0.

zoneinfo is part of the Python standard library from Python 3.9. The backports.zoneinfo package is automatically installed alongside Django if you are using Python 3.8.

Hardly any adjustments are required for the existing code. However, if you are working with non-UTC time zones, and using the pytz normalize() and localize() APIs, possibly with the TIME_ZONE setting, you will need to audit your code, since pytz and zoneinfo are not entirely equivalent.

# before
import pytz

# Now
import zoneinfo

Functional unique constraints

The new *expressions positional argument of UniqueConstraint() enables creating functional unique constraints on expressions and database functions. Functional unique constraints are added to models using the Meta.constraints option. For example:

from django.db import models
from django.db.models import UniqueConstraint
from django.db.models.functions import Lower

class MyModel(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)

    class Meta:
        constraints = [

Redis cache backend

Django recently offers direct integration for the in-memory database Redis as a cache via the RedisCache backend. redis-py 3.0.0 or higher is required.


      'default': {
            'BACKEND': 'django.core.cache.backends.redis.RedisCache',
            'LOCATION': 'redis://'

Template-based form rendering

Django already rendered changed to render form widgets through templates. Now the remaining pieces of the form process go through templates, which you can replace with custom display logic.


{{ errors }}

{% if errors and not fields %}
      <p>{% for field in hidden_fields %}{{ field }}{% endfor %}</p>
{% endif %}

{% for field, errors in fields %}

      {{ errors }}
      <p{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
      {% if field.label %}
            {{ field. label_tag }}
      {% endif %}
      {{ field }}
      {% if field.help_text %}
            <span class="helptext">{{ field.help_text }}</span>
      {% endif %}
      {% if forloop.last %}
            {% for field in hidden_fields %}{{ field }}{% endfor %}
      {% endif %}

{% endfor %}

{% if not fields and not errors %}
      {% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}


Looking for a python/django Development Team?

Share the details of your request and we will provide you with a full-cycle team under one roof.

Get an Estimate


Minor features


  • The admin/base.html template now has a new block header which contains the admin site header.
  • The new ModelAdmin.get_formset_kwargs() method allows customizing the keyword arguments passed to the constructor of a formset.
  • The navigation sidebar now has a quick filter toolbar.
  • The new context variable model which contains the model class for each model is added to the AdminSite.each_context() method.
  • The new ModelAdmin.search_help_text attribute allows specifying a descriptive text for the search box.
  • The InlineModelAdmin.verbose_name_plural attribute now fallbacks to the InlineModelAdmin.verbose_name + ‘s’.
  • jQuery is upgraded from version 3.5.1 to 3.6.0.



  • The default iteration count for the PBKDF2 password hasher is increased from 260,000 to 320,000.
  • The new LoginView.next_page attribute and get_default_redirect_url() method allow customizing the redirect after login.



  • The PostgreSQL backend now supports connecting by a service name. See PostgreSQL connection settings for more details.
  • The new AddConstraintNotValid operation allows creating check constraints on PostgreSQL without verifying that all existing rows satisfy the new constraint.
  • The new ValidateConstraint operation allows validating check constraints which were created using AddConstraintNotValid on PostgreSQL.
  • The new ArraySubquery() expression allows using subqueries to construct lists of values on PostgreSQL.
  • The new trigram_word_similar lookup, and the TrigramWordDistance() and TrigramWordSimilarity() expressions allow using trigram word similarity.



  • ModelChoiceField now includes the provided value in the params argument of a raised ValidationError for the invalid_choice error message. This allows custom error messages to use the %(value)s placeholder.
  • BaseFormSet now renders non-form errors with an additional class of nonform to help distinguish them from form-specific errors.
  • BaseFormSet now allows customizing the widget used when deleting forms via can_delete by setting the deletion_widget attribute or overriding get_deletion_widget() method.


Generic Views

  • DeleteView now uses FormMixin, allowing you to provide a Form subclass, with a checkbox for example, to confirm deletion. In addition, this allows DeleteView to function with django.contrib.messages.views.SuccessMessageMixin.


In accordance with FormMixin, object deletion for POST requests is handled in form_valid(). Custom delete logic in delete() handlers should be moved to form_valid(), or a shared helper method, as needed.


Management Commands

  • The runserver management command now supports the –skip-checks option.
  • On PostgreSQL, dbshell now supports specifying a password file.
  • The shell command now respects sys.__interactivehook__ at startup. This allows loading shell history between interactive sessions. As a consequence, readline is no longer loaded if running in isolated mode.
  • The new BaseCommand.suppressed_base_arguments attribute allows suppressing unsupported default command options in the help output.
  • The new startapp –exclude and startproject –exclude options allow excluding directories from the template.



  • New QuerySet.contains(obj) method returns whether the queryset contains the given object. This tries to perform the query in the simplest and fastest way possible.
  • The new precision argument of the Round() database function allows specifying the number of decimal places after rounding.
  • QuerySet.bulk_create() now sets the primary key on objects when using SQLite 3.35+.
  • DurationField now supports multiplying and dividing by scalar values on SQLite.
  • QuerySet.bulk_update() now returns the number of objects updated.
  • The new Expression.empty_result_set_value attribute allows specifying a value to return when the function is used over an empty result set.
  • The skip_locked argument of QuerySet.select_for_update() is now allowed on MariaDB 10.6+.
  • Lookup expressions may now be used in QuerySet annotations, aggregations, and directly in filters.
  • The new default argument for built-in aggregates allows specifying a value to be returned when the queryset (or grouping) contains no entries, rather than None.



  • The new serialized_aliases argument of django.test.utils.setup_databases() determines which DATABASES aliases test databases should have their state serialized to allow usage of the serialized_rollback feature.
  • Django test runner now supports a –buffer option with parallel tests.
  • The new logger argument to DiscoverRunner allows a Python logger to be used for logging.
  • The new DiscoverRunner.log() method provides a way to log messages that uses the DiscoverRunner.logger, or prints to the console if not set.
  • Django test runner now supports a –shuffle option to execute tests in a random order.
  • The test –parallel option now supports the value auto to run one test process for each processor core.
  • TestCase.captureOnCommitCallbacks() now captures new callbacks added while executing transaction.on_commit() callbacks.


Share this article