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.
# 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().
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.
# 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.
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',
),
]
