Qu’est-ce que Django ?
Django est un framework web pour Python dit « batteries-included » (clé en main). Alors que la plupart des frameworks vous demandent de choisir votre couche de base de données, votre moteur de templates, un panneau d’administration et un système d’authentification, Django vous fournit tout cela dans un ensemble cohérent. Vous décrivez vos données et vos pages, et le framework s’occupe des parties fastidieuses et sensibles en termes de sécurité.
Il a été conçu en 2003 pour la rédaction d’un journal — une équipe qui devait déployer des fonctionnalités rapidement sans craquer sous la pression des délais. Cette origine est flagrante : Django privilégie les conventions, l’explicite et une philosophie du type « il doit y avoir une seule manière évidente de faire les choses ». C’est pourquoi il reste l’un des choix les plus fiables pour les sites riches en contenu et pilotés par les données.
Si vous avez déjà passé une semaine entière à tenter de faire fonctionner ensemble un ORM, un outil de migration et un système d’authentification, Django est le framework qui vous fait gagner cette semaine.
Le pattern MTV
Django appelle son architecture MTV : Model, Template, View. C’est le même concept que le MVC, mais avec des noms différents, ce qui peut être déroutant au début.
- Un model est une classe Python qui décrit une table et ses lignes.
- Un template est un fichier HTML avec des placeholders qui permet d’afficher les données.
- Une view est une fonction ou une classe qui reçoit une requête, demande des données aux models et renvoie une réponse.
Le contrôleur, au sens classique du MVC, est Django lui-même. Le répartiteur d’URL (URL dispatcher) décide quelle view est exécutée, et le framework assure la médiation entre ces trois couches. Une fois ce schéma compris, tout le reste dans Django devient un simple détail plutôt qu’un mystère.
Modèles et ORM
Un modèle est une classe qui hérite de models.Model. Chaque attribut est un champ, et chaque champ devient une colonne.
# blog/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
body = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
CharField nécessite un max_length car il correspond à un VARCHAR borné. TextField n’est pas borné. auto_now_add=True définit une valeur uniquement lors de la création, tandis que auto_now=True se met à jour à chaque sauvegarde. null contrôle la base de données et blank contrôle la validation du formulaire ; ils sont séparés intentionnellement.
Les relations sont explicites :
ForeignKeyest une relation plusieurs-à-un (many-to-one).ManyToManyFieldcrée une table de jointure.OneToOneFieldest une clé étrangère unique.
Le related_name définit l’accesseur inverse, permettant ainsi à user.posts.all() de fonctionner depuis l’autre côté. L’argument on_delete est requis car Django refuse de deviner ce qui doit arriver aux enfants lorsqu’un parent est supprimé.
Migrations
Les migrations sont des fichiers versionnés qui décrivent les modifications apportées à votre schéma. Vous écrivez le modèle, Django écrit le SQL.
python manage.py makemigrations blog
python manage.py migrate
makemigrations inspecte vos modèles et produit une migration ; migrate applique les migrations en attente à la base de données. Comme les fichiers de migration résident dans votre dépôt, chaque environnement finit par avoir le même schéma, et les changements peuvent être revus comme n’importe quel autre code. Ne modifiez jamais une migration déjà appliquée — ajoutez-en plutôt une nouvelle.
Querysets et relations
L’ORM retourne des querysets, qui sont “lazy” (chargement différé) : ils ne sont pas exécutés tant que vous ne les itérez pas, ne les découpez pas (slice) ou ne les évaluez pas. Cette approche vous permet de chaîner des filtres et de n’exécuter qu’une seule requête finale.
Post.objects.filter(status="PB").exclude(author=user).order_by("-published_at")[:10]
Parmi les méthodes de queryset utiles, on trouve filter, exclude, get, first, exists, count, values, values_list, annotate et aggregate. Les expressions F() vous permettent de référencer une colonne de la base de données, et les objets Q() vous permettent de construire des conditions OR.
from django.db.models import Count, Q
published = Post.objects.filter(
Q(status="PB") | Q(published_at__isnull=False)
).annotate(comment_count=Count("comments"))
C’est là que l’ORM prouve toute son utilité : les requêtes complexes restent en Python, sont paramétrées de manière sécurisée et sont composables.
Éviter le problème du N+1
L’erreur de performance la plus courante dans Django est la requête N+1. Cela se produit lorsque vous accédez à un objet lié à l’intérieur d’une boucle, déclenchant ainsi une requête supplémentaire par ligne.
# 1 query for posts, then 1 query per post for the author
for post in Post.objects.all():
print(post.author.username)
select_related fusionne les relations à valeur unique dans la requête d’origine, tandis que prefetch_related exécute une seconde requête et assemble les résultats en Python pour les relations à valeurs multiples.
# 1 query for posts joined with author, 1 for the comments
posts = Post.objects.select_related("author").prefetch_related("comments")
Utilisez select_related pour les clés étrangères (foreign keys) et les champs one-to-one, et prefetch_related pour les relations many-to-many et les relations inverses. Si une page semble lente, comptez les requêtes avec la Django Debug Toolbar avant d’optimiser quoi que ce soit d’autre.
Vues : fonctions et classes
Une vue est tout élément appelable qui prend une requête en entrée et retourne une réponse. Les vues basées sur des fonctions sont le point de départ le plus simple.
# blog/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(status="PB").select_related("author")
return render(request, "blog/post_list.html", {"posts": posts})
def post_api(request, slug):
post = get_object_or_404(Post, slug=slug)
return JsonResponse({"title": post.title, "body": post.body})
Les vues basées sur des classes sacrifient un peu de directivité au profit de la réutilisation. ListView, DetailView, CreateView, UpdateView et DeleteView implémentent les modèles CRUD standards, et les mixins vous permettent de composer des comportements.
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status="PB").select_related("author")
Utilisez des vues basées sur des fonctions pour la logique ponctuelle et des vues basées sur des classes génériques pour les pages de liste et de détail standards. Mélanger les deux au sein d’une base de code est tout à fait normal et sain.
URLs
Le dispatcher d’URL mappe un chemin vers une vue. Les patterns se trouvent dans urls.py, et chaque application peut conserver les siens.
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]
Les convertisseurs tels que <int:id> et <slug:slug> valident et typent le segment capturé. Nommez toujours vos routes et effectuez l’ingénierie inverse avec reverse() ou le tag {% url %} au lieu de coder les chemins en dur, afin que les liens survivent à un renommage.
Templates
Les templates sont du HTML enrichi d’un langage volontairement limité. L’auto-échappement (autoescaping) est activé par défaut, ce qui vous protège des failles XSS.
{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }} on {{ post.published_at|date:"F j, Y" }}</p>
<div>{{ post.body|linebreaks }}</div>
{% endblock %}
{% extends %} permet l’héritage de templates, {% include %} permet de partager des fragments, et des filtres tels que date, linebreaks et default servent à formater les valeurs. Il n’est pas possible d’exécuter du code Python arbitraire dans les templates ; si un template nécessite de la logique, celle-ci doit se trouver dans la vue ou le modèle.
Formulaires et validation
Le système de formulaires de Django valide les entrées, génère le HTML et protège contre les falsifications grâce à un jeton CSRF.
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "status", "published_at"]
widgets = {"body": forms.Textarea(attrs={"rows": 12})}
# blog/views.py
from django.shortcuts import redirect, render
from .forms import PostForm
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
Un ModelForm dérive ses champs et sa validation du modèle, ce qui évite tout décalage entre les deux. Des méthodes de nettoyage comme clean_slug permettent d’ajouter des règles personnalisées, et form.is_valid() collecte toutes les erreurs avant même d’interagir avec la base de données.
L’interface d’administration
L’admin est la fonctionnalité la plus sous-estimée de Django. En enregistrant un modèle, vous obtenez une interface pour le personnel qui permet la recherche, le filtrage et la gestion des permissions.
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "published_at"]
list_filter = ["status", "created_at"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ["title"]}
raw_id_fields = ["author"]
date_hierarchy = "published_at"
C’est un outil interne complet en une douzaine de lignes. Pour les sites de contenu, l’admin est souvent le produit lui-même. C’est également une frontière de sécurité : les permissions du personnel, has_add_permission et has_change_permission contrôlent précisément ce que chaque rôle peut faire.
Paramètres et applications
Un projet Django est une collection d’apps, chacune étant un package autonome avec ses propres modèles, vues, templates et migrations. INSTALLED_APPS les liste, et settings.py contient la configuration.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog",
]
Évitez de stocker vos paramètres sensibles dans le contrôle de version. Lisez-les via des variables d’environnement, séparez les paramètres par environnement si le projet s’agrandit, et utilisez django-environ ou pydantic-settings pour les analyser. Les commandes django-admin startproject et startapp génèrent l’échafaudage de ces deux couches, vous n’avez donc jamais à vous soucier de la structure.
Paramètres de sécurité par défaut
Les réglages par défaut de Django sont conçus pour que la solution la plus sûre soit aussi la plus simple :
- Les jetons CSRF protègent les formulaires modifiant l’état ; le middleware rejette les requêtes qui n’en possèdent pas.
- L’injection SQL est évitée car les querysets paramètrent les valeurs.
- Les failles XSS sont atténuées car les templates échappent les variables, sauf si vous choisissez de ne pas le faire avec
|safe. - Les mots de passe sont hachés avec un algorithme moderne et des hashers configurables.
- Le Clickjacking est bloqué par le middleware
X-Frame-Options. - Le HTTPS est imposé en production via
SECURE_SSL_REDIRECT,SESSION_COOKIE_SECUREetCSRF_COOKIE_SECURE.
Exécutez python manage.py check --deploy avant le déploiement. Cela signalera les paramètres que vous avez oubliés, ce qui est bien moins coûteux qu’un audit de sécurité.
Créer des API avec Django REST Framework
Pour les API JSON, Django REST Framework (DRF) est le compagnon standard. Un serializer décrit comment un modèle est converti en JSON et inversement, tandis qu’un viewset associé à un router génère les endpoints CRUD.
# blog/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Post
fields = ["id", "title", "slug", "author", "status", "published_at"]
# blog/api.py
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related("author")
serializer_class = PostSerializer
lookup_field = "slug"
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .api import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = router.urls
Les serializers valident les entrées de la même manière que les formulaires, les viewsets fournissent nativement les actions de liste, création, récupération, mise à jour et suppression, et DRF peut générer des schémas OpenAPI pour votre API. L’ORM de Django et les conventions de DRF constituent une voie éprouvée pour les API en production.
Tests
Le test runner de Django crée une base de données de test vierge et fournit un client capable de simuler l’intégralité du pipeline de requête.
# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import Post
class PostDetailTests(TestCase):
def setUp(self):
user = get_user_model().objects.create_user("ada", password="secret")
self.post = Post.objects.create(
title="Hello",
slug="hello",
body="First post",
author=user,
status=Post.Status.PUBLISHED,
)
def test_detail_page_renders(self):
response = self.client.get(reverse("blog:post_detail", args=["hello"]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hello")
def test_missing_post_returns_404(self):
response = self.client.get(reverse("blog:post_detail", args=["nope"]))
self.assertEqual(response.status_code, 404)
TestCase enveloppe chaque test dans une transaction et effectue un rollback, garantissant ainsi que les tests restent isolés et rapides. Utilisez django.test.Client pour les vues, l’ORM directement pour les modèles, et pytest-django si vous préférez les fixtures de pytest. Concentrez-vous sur le test du comportement et des permissions, plutôt que sur les détails d’implémentation.
Bonnes pratiques
- Gardez vos modèles concentrés sur les données et les petites méthodes de domaine ; placez l’orchestration dans les services ou les vues.
- Exécutez toujours
select_relatedetprefetch_relateddès qu’un template manipule des relations. - Utilisez
ModelFormet les serializers de DRF afin que la validation soit centralisée au même endroit. - Nommez chaque URL et utilisez le mécanisme de résolution inverse (reverse) ; ne codez jamais les chemins en dur dans les templates ou le code.
- Ajoutez des index de base de données pour les colonnes sur lesquelles vous effectuez des filtres ou des tris.
- Conservez vos secrets dans des variables d’environnement et exécutez
check --deployavant la mise en production. - Considérez les migrations comme des artefacts de revue de code et ne modifiez jamais une migration déjà appliquée.
- Testez le pipeline de requête avec
Clientet testez la logique métier directement.
Erreurs courantes
- Itérer sur un queryset et modifier des relations à l’intérieur de la boucle, provoquant des requêtes N+1.
- Utiliser
null=Truesur un champ texte au lieu d’une chaîne vide, puis se battre avec les vérificationsNone. - Placer la logique métier dans les templates parce que la vue semblait trop chargée.
- Appeler
save()dans une boucle au lieu debulk_createoubulk_update. - Oublier
on_deleteou choisirCASCADEsans réfléchir à la perte de données. - Laisser
DEBUG = Trueet unSECRET_KEYcodé en dur en production. - Traiter l’admin comme un frontend destiné aux utilisateurs plutôt que comme un outil pour le staff.
- Effectuer des requêtes avec
len(queryset)au lieu de.count(), chargeant ainsi chaque ligne en mémoire.
Et après ?
Django vous offre un parcours complet et conventionnel pour créer une application web Python prête pour la production. Si vous préférez un cœur plus léger et plus de liberté dans vos choix, consultez ensuite le guide Flask. Si vous construisez une API JSON asynchrone moderne et que vous souhaitez utiliser les types pour la validation, tournez-vous vers FastAPI. Quel que soit votre choix, investissez dans votre base de données : l’indexation, les transactions et la planification des requêtes sont plus cruciales que le framework lui-même, et la roadmap backend approfondit ces notions. Le guide sur les REST APIs détaille les règles de conception qui rendent une API agréable à consommer.