Vue d'ensemble

Django 4.0, publié le 7 décembre 2021, ouvre un nouveau cycle majeur du framework. Cette version modernise la gestion des fuseaux horaires en adoptant zoneinfo de la bibliothèque standard, introduit un backend de cache Redis intégré, et renforce les contraintes de base de données avec des expressions dans les conditions.

Django 4.0 abandonne également le support de Python 3.6 et 3.7, exigeant désormais Python 3.8 au minimum. Le hasheur de mots de passe scrypt fait son apparition comme alternative plus résistante aux attaques matérielles.

Fonctionnalités principales

Backend de cache Redis

Django intègre désormais un backend de cache Redis natif via django.core.cache.backends.redis.RedisCache. Jusqu'ici, il fallait installer un paquet tiers comme django-redis. Le nouveau backend s'appuie sur redis-py et supporte la configuration par URL, le mode sentinelle et les clusters.

python
# settings.py — Configuration du cache Redis intégré
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379',
    },
    'sessions': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'TIMEOUT': 86400,  # 24 heures
    },
}

# Utilisation dans une vue
from django.core.cache import cache

def catalogue_produits(request):
    produits = cache.get('produits_actifs')
    if produits is None:
        produits = list(
            Produit.objects.filter(actif=True)
            .select_related('categorie')
            .values('nom', 'prix', 'categorie__nom')
        )
        cache.set('produits_actifs', produits, timeout=300)
    return render(request, 'catalogue.html', {'produits': produits})

# Invalidation ciblée lors d'une modification
def mettre_a_jour_produit(request, produit_id):
    produit = Produit.objects.get(pk=produit_id)
    produit.prix = request.POST['nouveau_prix']
    produit.save()
    cache.delete('produits_actifs')
    return redirect('catalogue')

zoneinfo par défaut

Django utilise désormais le module zoneinfo de la bibliothèque standard Python (introduit en Python 3.9) au lieu de pytz. Le paramètre USE_DEPRECATED_PYTZ permet une migration progressive. L'interface des objets zoneinfo est plus intuitive : on n'a plus besoin d'appeler localize() ni normalize().

python
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

# Création d'une date avec fuseau horaire
paris = ZoneInfo('Europe/Paris')
reunion = datetime(2022, 3, 15, 14, 30, tzinfo=paris)
print(reunion)  # 2022-03-15 14:30:00+01:00

# Conversion vers un autre fuseau horaire
new_york = ZoneInfo('America/New_York')
reunion_ny = reunion.astimezone(new_york)
print(reunion_ny)  # 2022-03-15 09:30:00-04:00

# Plus besoin de localize() comme avec pytz
from django.utils import timezone

maintenant = timezone.now()  # utilise zoneinfo en interne
print(maintenant.tzinfo)  # zoneinfo.ZoneInfo(key='UTC')

# settings.py — migration progressive
# USE_DEPRECATED_PYTZ = True  # pour garder pytz temporairement
TIME_ZONE = 'Europe/Paris'
USE_TZ = True

Hasheur scrypt

Le nouveau hasheur ScryptPasswordHasher utilise l'algorithme scrypt, conçu pour être coûteux en mémoire et ainsi résister aux attaques par GPU ou ASIC. Il vient s'ajouter aux hasheurs existants comme PBKDF2, bcrypt et Argon2.

python
# settings.py — Ajouter scrypt en tête de liste
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.ScryptPasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]

# Vérification manuelle
from django.contrib.auth.hashers import make_password, check_password

hashed = make_password('mon_mot_de_passe_secret')
print(hashed[:20])  # scrypt$16384$8$1$...

# Le hasheur est sélectionné automatiquement
assert check_password('mon_mot_de_passe_secret', hashed)

# Les anciens hashs PBKDF2 restent valides
# et sont automatiquement re-hashés en scrypt à la connexion

Expressions dans les contraintes

Les classes CheckConstraint et UniqueConstraint acceptent désormais des expressions complètes, pas seulement des objets Q. Cela permet de créer des contraintes calculant des valeurs avec des fonctions de base de données, des index partiels et des conditions plus sophistiquées.

python
from django.db import models
from django.db.models import Q, F, UniqueConstraint, CheckConstraint
from django.db.models.functions import Lower

class Evenement(models.Model):
    titre = models.CharField(max_length=200)
    date_debut = models.DateTimeField()
    date_fin = models.DateTimeField()
    lieu = models.CharField(max_length=100)
    capacite = models.PositiveIntegerField()
    inscrits = models.PositiveIntegerField(default=0)
    annule = models.BooleanField(default=False)

    class Meta:
        constraints = [
            # La date de fin doit être postérieure à la date de début
            CheckConstraint(
                check=Q(date_fin__gt=F('date_debut')),
                name='evenement_fin_apres_debut',
            ),
            # Le nombre d'inscrits ne peut dépasser la capacité
            CheckConstraint(
                check=Q(inscrits__lte=F('capacite')),
                name='evenement_inscrits_max',
            ),
            # Unicité du titre en minuscules (insensible à la casse)
            UniqueConstraint(
                Lower('titre'),
                name='evenement_titre_unique_ci',
            ),
            # Un seul événement actif par lieu et créneau
            UniqueConstraint(
                fields=['lieu', 'date_debut'],
                condition=Q(annule=False),
                name='evenement_unique_lieu_actif',
            ),
        ]

Sources