Questions d'entretien Django et Python : Top 25 en 2026

Les 25 questions d'entretien Django et Python les plus posées. ORM, vues, middleware, DRF, signaux et optimisation avec réponses détaillées et exemples de code.

Questions d'entretien Django et Python - Guide complet

Les entretiens Django évaluent la maîtrise du framework web Python le plus populaire, la compréhension de l'ORM, l'architecture MVT, et la capacité à construire des API REST robustes. Ce guide couvre les 25 questions les plus posées, des fondamentaux Django jusqu'aux patterns avancés de Django REST Framework.

Conseil pour l'entretien

Les recruteurs apprécient les candidats qui expliquent les choix architecturaux de Django. Comprendre pourquoi le framework adopte certaines conventions (convention over configuration) fait la différence en entretien.

Fondamentaux Django

Question 1 : Décrivez l'architecture MVT de Django

L'architecture Model-View-Template (MVT) est la variante Django du pattern MVC. Le framework gère automatiquement la partie contrôleur, simplifiant le développement.

python
# models.py
# Le Model représente la structure des données et la logique métier
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published_at = models.DateTimeField(auto_now_add=True)
    author = models.ForeignKey(
        "auth.User",
        on_delete=models.CASCADE,  # Supprime les articles si l'auteur est supprimé
        related_name="articles"     # Accès inverse : user.articles.all()
    )

    class Meta:
        ordering = ["-published_at"]  # Tri par défaut

    def __str__(self):
        return self.title
python
# views.py
# La View contient la logique de traitement des requêtes
from django.shortcuts import render, get_object_or_404

def article_detail(request, pk):
    # get_object_or_404 lève Http404 si l'objet n'existe pas
    article = get_object_or_404(Article, pk=pk)
    return render(request, "blog/article_detail.html", {"article": article})

Dans MVT, Django joue le rôle de contrôleur en routant les URLs vers les vues appropriées via urls.py. Le Template gère la présentation HTML.

Question 2 : Quelle est la différence entre un projet et une application Django ?

Un projet est la configuration globale (settings, URLs racine, WSGI/ASGI). Une application est un module réutilisable avec une responsabilité unique. Un projet contient plusieurs applications.

python
# Créer un projet et une application
# django-admin startproject myproject
# python manage.py startapp blog

# settings.py
# Enregistrement des applications dans le projet
INSTALLED_APPS = [
    "django.contrib.admin",       # Interface d'administration
    "django.contrib.auth",        # Authentification
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # Applications personnalisées
    "blog.apps.BlogConfig",       # Application blog
    "users.apps.UsersConfig",     # Application utilisateurs
]

Chaque application suit le principe de responsabilité unique et peut être réutilisée dans d'autres projets.

Question 3 : Expliquez le cycle de vie d'une requête Django

La requête traverse plusieurs couches avant d'atteindre la vue. Comprendre ce cycle est essentiel pour le debugging et l'optimisation.

python
# middleware.py
# Les middlewares interceptent chaque requête/réponse
class RequestTimingMiddleware:
    """Middleware qui mesure le temps de traitement."""

    def __init__(self, get_response):
        self.get_response = get_response  # Référence au middleware suivant

    def __call__(self, request):
        import time
        start = time.time()

        # Phase requête : avant la vue
        response = self.get_response(request)

        # Phase réponse : après la vue
        duration = time.time() - start
        response["X-Request-Duration"] = f"{duration:.3f}s"
        return response

Le cycle complet : requête HTTP → WSGI/ASGI → middlewares (process_request) → URL resolver → vue → middlewares (process_response) → réponse HTTP.

ORM et Base de Données

Question 4 : Comment fonctionne le QuerySet de Django et quel est son lazy loading ?

Les QuerySets sont évalués paresseusement : aucune requête SQL n'est exécutée tant que les données ne sont pas effectivement utilisées.

python
# queryset_lazy.py
# Démonstration du lazy loading des QuerySets

# Aucune requête SQL n'est exécutée ici
qs = Article.objects.filter(published=True)  # Pas de requête
qs = qs.exclude(title="Draft")               # Toujours pas
qs = qs.order_by("-created_at")              # Toujours pas

# La requête SQL est exécutée UNIQUEMENT ici
for article in qs:  # UNE seule requête SQL combinée
    print(article.title)

# Autres déclencheurs d'évaluation
list(qs)        # Conversion en liste
qs[0]           # Accès par index
len(qs)         # Comptage (préférer qs.count())
bool(qs)        # Test d'existence (préférer qs.exists())

Cette évaluation paresseuse permet de chaîner les filtres sans surcoût, et de n'exécuter qu'une seule requête optimisée.

Question 5 : Qu'est-ce que le problème N+1 et comment le résoudre ?

Le problème N+1 survient quand une requête principale génère N requêtes supplémentaires pour charger les relations. C'est la cause la plus fréquente de lenteur dans les applications Django.

python
# n_plus_one.py
# Problème N+1 et solutions

# ❌ PROBLÈME : N+1 requêtes
# 1 requête pour les articles + 1 requête PAR article pour l'auteur
articles = Article.objects.all()
for article in articles:
    print(article.author.username)  # Requête SQL à chaque itération !

# ✅ SOLUTION 1 : select_related (ForeignKey, OneToOne)
# Joint les tables en UNE seule requête SQL (JOIN)
articles = Article.objects.select_related("author").all()
for article in articles:
    print(article.author.username)  # Pas de requête supplémentaire

# ✅ SOLUTION 2 : prefetch_related (ManyToMany, reverse FK)
# Exécute 2 requêtes séparées + assemblage Python
articles = Article.objects.prefetch_related("tags").all()
for article in articles:
    print(article.tags.all())  # Données déjà en cache

# ✅ SOLUTION 3 : Prefetch personnalisé avec filtre
from django.db.models import Prefetch

articles = Article.objects.prefetch_related(
    Prefetch(
        "comments",
        queryset=Comment.objects.filter(approved=True).select_related("user"),
        to_attr="approved_comments"  # Attribut personnalisé
    )
)

Utiliser select_related pour les relations ForeignKey/OneToOne (JOIN SQL) et prefetch_related pour ManyToMany ou les relations inverses (requêtes séparées).

Question 6 : Comment créer un Manager personnalisé et quand l'utiliser ?

Les Managers personnalisés encapsulent les requêtes fréquentes au niveau du modèle, rendant le code plus lisible et réutilisable.

python
# managers.py
# Managers et QuerySets personnalisés

class PublishedQuerySet(models.QuerySet):
    """QuerySet réutilisable pour les articles publiés."""

    def published(self):
        return self.filter(status="published", published_at__lte=timezone.now())

    def by_author(self, user):
        return self.filter(author=user)

    def popular(self):
        return self.annotate(
            comment_count=models.Count("comments")
        ).order_by("-comment_count")


class PublishedManager(models.Manager):
    """Manager qui expose uniquement les articles publiés."""

    def get_queryset(self):
        return PublishedQuerySet(self.model, using=self._db).published()


class Article(models.Model):
    # ...
    objects = models.Manager()          # Manager par défaut (tous les articles)
    published = PublishedManager()      # Manager personnalisé (publiés uniquement)

    # Utilisation :
    # Article.objects.all()             → Tous les articles
    # Article.published.all()           → Articles publiés uniquement
    # Article.published.popular()       → Articles publiés triés par popularité

Les Managers personnalisés respectent le principe DRY et centralisent la logique de requête.

Prêt à réussir tes entretiens Django ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Vues et URLs

Question 7 : Quand utiliser les Class-Based Views vs Function-Based Views ?

Les Function-Based Views (FBV) offrent simplicité et contrôle explicite. Les Class-Based Views (CBV) apportent réutilisabilité et structure via l'héritage.

python
# views_comparison.py
# FBV : Explicite, simple, facile à comprendre
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def article_list(request):
    if request.method == "GET":
        articles = Article.published.all()
        return render(request, "articles/list.html", {"articles": articles})

    # POST : création d'article
    form = ArticleForm(request.POST)
    if form.is_valid():
        form.save()
        return redirect("article-list")
    return render(request, "articles/list.html", {"form": form})
python
# views_cbv.py
# CBV : Réutilisable, extensible via mixins
from django.views.generic import ListView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin

class ArticleListView(LoginRequiredMixin, ListView):
    model = Article
    template_name = "articles/list.html"
    context_object_name = "articles"    # Nom de la variable dans le template
    paginate_by = 20                     # Pagination automatique

    def get_queryset(self):
        # Surcharge pour filtrer les articles publiés
        return Article.published.all()

class ArticleCreateView(LoginRequiredMixin, CreateView):
    model = Article
    form_class = ArticleForm
    success_url = reverse_lazy("article-list")

    def form_valid(self, form):
        form.instance.author = self.request.user  # Assigne l'auteur
        return super().form_valid(form)

Règle pratique : utiliser les FBV pour la logique simple ou non standard, les CBV pour les opérations CRUD classiques.

Question 8 : Comment fonctionnent les middlewares dans Django ?

Les middlewares sont des hooks qui traitent chaque requête/réponse. Chaque middleware peut intervenir à différentes phases du cycle de traitement.

python
# auth_middleware.py
# Middleware d'authentification personnalisé
import jwt
from django.conf import settings
from django.http import JsonResponse

class JWTAuthenticationMiddleware:
    """Vérifie le token JWT sur les endpoints protégés."""

    EXEMPT_PATHS = ["/api/auth/login", "/api/auth/register", "/health"]

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Ignorer les chemins exemptés
        if any(request.path.startswith(p) for p in self.EXEMPT_PATHS):
            return self.get_response(request)

        # Extraire et vérifier le token
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return JsonResponse({"error": "Token manquant"}, status=401)

        try:
            token = auth_header.split(" ")[1]
            payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
            request.user_id = payload["user_id"]  # Attache au request
        except jwt.ExpiredSignatureError:
            return JsonResponse({"error": "Token expiré"}, status=401)
        except jwt.InvalidTokenError:
            return JsonResponse({"error": "Token invalide"}, status=401)

        return self.get_response(request)

L'ordre des middlewares dans MIDDLEWARE est crucial : ils s'exécutent de haut en bas pour les requêtes et de bas en haut pour les réponses.

Django REST Framework

Question 9 : Quelle est la différence entre Serializer et ModelSerializer ?

Le Serializer définit manuellement chaque champ, tandis que ModelSerializer génère automatiquement les champs depuis le modèle.

python
# serializers.py
from rest_framework import serializers

# Serializer manuel : contrôle total sur chaque champ
class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    content = serializers.CharField()
    author_name = serializers.SerializerMethodField()

    def get_author_name(self, obj):
        return obj.author.get_full_name()

    def create(self, validated_data):
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.title = validated_data.get("title", instance.title)
        instance.content = validated_data.get("content", instance.content)
        instance.save()
        return instance


# ModelSerializer : génération automatique des champs
class ArticleModelSerializer(serializers.ModelSerializer):
    author_name = serializers.SerializerMethodField()
    comment_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Article
        fields = ["id", "title", "content", "author", "author_name",
                  "comment_count", "published_at"]
        read_only_fields = ["published_at"]

    def get_author_name(self, obj):
        return obj.author.get_full_name()

Préférer ModelSerializer pour les cas standard et Serializer quand la représentation diffère significativement du modèle.

Question 10 : Comment implémenter la pagination dans DRF ?

DRF propose plusieurs stratégies de pagination configurables globalement ou par vue.

python
# settings.py
# Configuration globale de la pagination
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

# pagination.py
# Pagination personnalisée par vue
from rest_framework.pagination import CursorPagination, LimitOffsetPagination

class ArticleCursorPagination(CursorPagination):
    """Pagination par curseur : performante pour les grands datasets."""
    page_size = 20
    ordering = "-published_at"  # Champ indexé requis
    cursor_query_param = "cursor"

class ArticleLimitOffsetPagination(LimitOffsetPagination):
    """Pagination offset/limit : flexible mais moins performante."""
    default_limit = 20
    max_limit = 100

# views.py
class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.published.all()
    serializer_class = ArticleModelSerializer
    pagination_class = ArticleCursorPagination  # Pagination spécifique

La pagination par curseur est recommandée pour les datasets volumineux car elle reste performante quelle que soit la page, contrairement à OFFSET/LIMIT.

Question 11 : Comment sécuriser une API avec les permissions DRF ?

DRF propose un système de permissions modulaire combinant authentification et autorisations granulaires.

python
# permissions.py
from rest_framework.permissions import BasePermission, IsAuthenticated

class IsAuthorOrReadOnly(BasePermission):
    """Seul l'auteur peut modifier, tout le monde peut lire."""

    def has_object_permission(self, request, view, obj):
        # GET, HEAD, OPTIONS sont toujours autorisés
        if request.method in ("GET", "HEAD", "OPTIONS"):
            return True
        # Seul l'auteur peut modifier ou supprimer
        return obj.author == request.user


class IsAdminOrManager(BasePermission):
    """Accès réservé aux admins et managers."""

    def has_permission(self, request, view):
        return (
            request.user.is_authenticated
            and request.user.role in ("admin", "manager")
        )


# views.py
from rest_framework.viewsets import ModelViewSet
from rest_framework.throttling import UserRateThrottle

class ArticleViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
    throttle_classes = [UserRateThrottle]  # Rate limiting

    def get_permissions(self):
        # Permissions dynamiques selon l'action
        if self.action == "destroy":
            return [IsAdminOrManager()]
        return super().get_permissions()

Combiner permission_classes au niveau de la vue et has_object_permission pour un contrôle fin objet par objet.

Signaux et Tâches Asynchrones

Question 12 : Comment fonctionnent les signaux Django et quand les utiliser ?

Les signaux permettent de découpler les composants en réagissant à des événements du framework ou de l'application.

python
# signals.py
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.core.mail import send_mail

@receiver(post_save, sender=Article)
def notify_on_publish(sender, instance, created, **kwargs):
    """Envoie une notification quand un article est publié."""
    if not created and instance.status == "published":
        # Déclenché uniquement lors de la publication (pas la création)
        subscribers = instance.author.subscribers.values_list("email", flat=True)
        send_mail(
            subject=f"Nouvel article : {instance.title}",
            message=f"Découvrez le dernier article de {instance.author.username}",
            from_email="noreply@example.com",
            recipient_list=list(subscribers),
        )

@receiver(pre_delete, sender=Article)
def cleanup_article_files(sender, instance, **kwargs):
    """Supprime les fichiers associés avant la suppression de l'article."""
    if instance.cover_image:
        instance.cover_image.delete(save=False)  # Supprime le fichier physique

Les signaux conviennent pour les effets de bord légers (logging, cache invalidation). Pour les tâches lourdes, préférer Celery.

Question 13 : Comment intégrer Celery avec Django pour les tâches asynchrones ?

Celery permet d'exécuter des tâches en arrière-plan, essentiel pour les opérations longues comme l'envoi d'emails ou le traitement de fichiers.

python
# celery_config.py
# Configuration Celery dans le projet Django
import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

app = Celery("myproject")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()  # Découvre les tasks.py dans chaque app

# tasks.py
from celery import shared_task
from django.core.mail import send_mass_mail

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_newsletter(self, article_id):
    """Envoie la newsletter de manière asynchrone."""
    try:
        article = Article.objects.get(id=article_id)
        subscribers = User.objects.filter(newsletter=True)

        messages = [
            (f"Nouveau : {article.title}", article.content[:200],
             "noreply@example.com", [sub.email])
            for sub in subscribers
        ]
        send_mass_mail(messages, fail_silently=False)
    except Article.DoesNotExist:
        pass  # L'article a été supprimé entre-temps
    except Exception as exc:
        self.retry(exc=exc)  # Retry automatique en cas d'erreur

# Appel depuis une vue
# send_newsletter.delay(article.id)  # Exécution asynchrone
# send_newsletter.apply_async(args=[article.id], countdown=300)  # Délai de 5 min

Celery est indispensable en production pour toute opération qui ne doit pas bloquer la réponse HTTP.

Sécurité et Authentification

Question 14 : Comment Django protège-t-il contre les attaques CSRF ?

Django intègre une protection CSRF par défaut via un middleware qui vérifie un token unique à chaque requête POST.

python
# Protection CSRF dans les formulaires
# Le template tag {% csrf_token %} génère un champ hidden

# Pour les API (DRF), CSRF est souvent désactivé en faveur de tokens
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",  # Inclut CSRF
        "rest_framework.authentication.TokenAuthentication",     # Pas de CSRF
    ],
}

# Pour les vues AJAX avec session auth
# Le cookie csrftoken doit être envoyé dans le header X-CSRFToken
python
# csrf_exemption.py
# Exempter une vue spécifique (avec précaution)
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie

@ensure_csrf_cookie
def get_csrf_token(request):
    """Endpoint qui force l'envoi du cookie CSRF au client."""
    return JsonResponse({"detail": "CSRF cookie set"})

@csrf_exempt  # ⚠️ À utiliser uniquement pour les webhooks externes
def stripe_webhook(request):
    """Webhook Stripe - authentifié par signature, pas par CSRF."""
    payload = request.body
    sig_header = request.headers.get("Stripe-Signature")
    # Vérification par signature Stripe à la place

Ne jamais désactiver CSRF globalement. Utiliser csrf_exempt uniquement sur les endpoints authentifiés autrement (webhooks, API tokens).

Question 15 : Comment implémenter l'authentification personnalisée dans Django ?

Django permet de remplacer le modèle User par défaut et de personnaliser le backend d'authentification.

python
# models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin

class CustomUserManager(BaseUserManager):
    """Manager pour le modèle User personnalisé."""

    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("L'email est obligatoire")
        email = self.normalize_email(email)  # Normalise le domaine
        user = self.model(email=email, **extra_fields)
        user.set_password(password)  # Hash le mot de passe
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        return self.create_user(email, password, **extra_fields)


class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)          # Login par email
    username = models.CharField(max_length=30, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)

    objects = CustomUserManager()

    USERNAME_FIELD = "email"      # Champ utilisé pour le login
    REQUIRED_FIELDS = []          # Champs requis en plus de USERNAME_FIELD

# settings.py
AUTH_USER_MODEL = "users.CustomUser"  # Avant la première migration !

Définir AUTH_USER_MODEL dès le début du projet. Le changer après les premières migrations est complexe et risqué.

Optimisation et Performance

Question 16 : Comment optimiser les requêtes ORM Django ?

L'optimisation des requêtes est critique pour la performance. Plusieurs techniques réduisent le nombre et le coût des requêtes SQL.

python
# query_optimization.py
from django.db.models import F, Q, Count, Avg, Prefetch

# 1. Only/Defer : charger uniquement les champs nécessaires
articles = Article.objects.only("title", "published_at")  # SELECT title, published_at
heavy_articles = Article.objects.defer("content")          # Tout SAUF content

# 2. Agrégations au niveau SQL (pas Python)
stats = Article.objects.aggregate(
    total=Count("id"),
    avg_views=Avg("view_count"),
)

# 3. F() expressions : opérations au niveau SQL
Article.objects.filter(published=True).update(
    view_count=F("view_count") + 1  # Incrémentation atomique en SQL
)

# 4. Q() objects : requêtes complexes
results = Article.objects.filter(
    Q(title__icontains="django") | Q(tags__name="python"),
    ~Q(status="draft"),  # NOT draft
    published_at__year=2026
)

# 5. Bulk operations : réduire les requêtes INSERT/UPDATE
articles = [Article(title=f"Article {i}") for i in range(100)]
Article.objects.bulk_create(articles, batch_size=50)  # 2 requêtes au lieu de 100

Article.objects.filter(status="draft").update(status="archived")  # 1 requête

Utiliser django-debug-toolbar en développement pour identifier les requêtes lentes et le problème N+1.

Question 17 : Comment implémenter le caching dans Django ?

Django propose un framework de cache multi-niveaux : par vue, par template fragment, ou par données arbitraires.

python
# settings.py
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
    }
}

# cache_strategies.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

# Cache par vue : met en cache la réponse HTTP entière
@cache_page(60 * 15)  # 15 minutes
def article_list(request):
    return render(request, "articles/list.html", {"articles": Article.published.all()})

# Cache de données : contrôle granulaire
def get_popular_articles():
    cache_key = "popular_articles_v1"
    articles = cache.get(cache_key)

    if articles is None:
        articles = list(
            Article.published.popular()[:10].values("id", "title", "view_count")
        )
        cache.set(cache_key, articles, timeout=60 * 30)  # 30 min

    return articles

# Invalidation du cache
def invalidate_article_cache(article_id):
    cache.delete(f"article_{article_id}")
    cache.delete("popular_articles_v1")
    cache.delete_pattern("article_list_*")  # Avec django-redis

Privilégier Redis comme backend de cache en production pour la persistance et les fonctionnalités avancées (patterns, TTL).

Prêt à réussir tes entretiens Django ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Migrations et Gestion de la Base

Question 18 : Comment gérer les migrations complexes dans Django ?

Les migrations Django gèrent l'évolution du schéma de base de données de manière versionnée et reproductible.

python
# 0005_migrate_data.py
# Migration de données personnalisée
from django.db import migrations

def migrate_user_roles(apps, schema_editor):
    """Convertit les booléens is_admin en rôles textuels."""
    User = apps.get_model("users", "CustomUser")
    # Utiliser apps.get_model() pour accéder au modèle historique
    User.objects.filter(is_admin=True).update(role="admin")
    User.objects.filter(is_admin=False, is_staff=True).update(role="manager")
    User.objects.filter(is_admin=False, is_staff=False).update(role="user")

def reverse_migrate(apps, schema_editor):
    """Migration inverse pour le rollback."""
    User = apps.get_model("users", "CustomUser")
    User.objects.filter(role="admin").update(is_admin=True)

class Migration(migrations.Migration):
    dependencies = [
        ("users", "0004_add_role_field"),
    ]

    operations = [
        migrations.RunPython(migrate_user_roles, reverse_migrate),
    ]

Toujours fournir une fonction reverse pour permettre le rollback. Tester les migrations sur une copie de la base de production avant le déploiement.

Question 19 : Comment créer des indexes personnalisés pour l'optimisation ?

Les indexes accélèrent les requêtes fréquentes mais augmentent le coût des écritures. Un choix judicieux est essentiel.

python
# models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    status = models.CharField(max_length=20, db_index=True)  # Index simple
    published_at = models.DateTimeField(null=True)
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)

    class Meta:
        indexes = [
            # Index composé pour les requêtes fréquentes
            models.Index(fields=["status", "-published_at"], name="idx_status_date"),
            # Index partiel : uniquement les articles publiés
            models.Index(
                fields=["published_at"],
                condition=models.Q(status="published"),
                name="idx_published_articles"
            ),
            # Index GIN pour la recherche full-text (PostgreSQL)
            GinIndex(fields=["search_vector"], name="idx_search"),
        ]

Les indexes composés suivent l'ordre des colonnes : le champ le plus sélectif doit être en premier.

Tests et Qualité

Question 20 : Comment structurer les tests dans un projet Django ?

Django fournit un framework de test robuste basé sur unittest, enrichi par pytest-django pour plus de flexibilité.

python
# tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient

class ArticleViewTests(TestCase):
    """Tests des vues avec le client Django."""

    def setUp(self):
        self.client = Client()
        self.user = CustomUser.objects.create_user(
            email="test@example.com", password="testpass123"
        )
        self.article = Article.objects.create(
            title="Test Article",
            content="Content here",
            author=self.user,
            status="published"
        )

    def test_article_list_returns_200(self):
        response = self.client.get(reverse("article-list"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Test Article")

    def test_create_article_requires_auth(self):
        response = self.client.post(reverse("article-create"), {"title": "New"})
        self.assertEqual(response.status_code, 302)  # Redirige vers login


class ArticleAPITests(APITestCase):
    """Tests de l'API REST avec DRF."""

    def setUp(self):
        self.user = CustomUser.objects.create_user(
            email="api@example.com", password="testpass123"
        )
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_create_article_via_api(self):
        data = {"title": "API Article", "content": "Created via API"}
        response = self.client.post("/api/articles/", data, format="json")
        self.assertEqual(response.status_code, 201)
        self.assertEqual(Article.objects.count(), 1)

Séparer les tests en fichiers par domaine : test_models.py, test_views.py, test_serializers.py, test_services.py.

Question 21 : Comment utiliser les fixtures et factories pour les tests ?

Les factories (avec factory_boy) sont préférées aux fixtures JSON pour la flexibilité et la maintenabilité des données de test.

python
# factories.py
import factory
from factory.django import DjangoModelFactory

class UserFactory(DjangoModelFactory):
    class Meta:
        model = CustomUser

    email = factory.Sequence(lambda n: f"user{n}@example.com")
    username = factory.Faker("user_name")
    is_active = True

class ArticleFactory(DjangoModelFactory):
    class Meta:
        model = Article

    title = factory.Faker("sentence", nb_words=5)
    content = factory.Faker("paragraphs", nb=3)
    author = factory.SubFactory(UserFactory)  # Crée un user automatiquement
    status = "published"

    class Params:
        draft = factory.Trait(status="draft", published_at=None)

# tests.py
def test_published_articles_count(self):
    ArticleFactory.create_batch(5)              # 5 articles publiés
    ArticleFactory.create_batch(3, draft=True)  # 3 brouillons
    self.assertEqual(Article.published.count(), 5)

Les factories garantissent des données de test cohérentes et évitent les dépendances entre tests.

Patterns Avancés

Question 22 : Comment implémenter les WebSockets avec Django Channels ?

Django Channels étend Django au-delà de HTTP pour supporter les WebSockets, les protocoles temps réel et les tâches en arrière-plan.

python
# consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    """Consumer WebSocket pour un chat en temps réel."""

    async def connect(self):
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        self.room_group = f"chat_{self.room_name}"

        # Rejoindre le groupe de la room
        await self.channel_layer.group_add(self.room_group, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        # Quitter le groupe
        await self.channel_layer.group_discard(self.room_group, self.channel_name)

    async def receive(self, text_data):
        data = json.loads(text_data)
        # Diffuser le message à tout le groupe
        await self.channel_layer.group_send(
            self.room_group,
            {"type": "chat.message", "message": data["message"],
             "username": self.scope["user"].username}
        )

    async def chat_message(self, event):
        # Envoie le message au client WebSocket
        await self.send(text_data=json.dumps({
            "message": event["message"],
            "username": event["username"],
        }))

Django Channels utilise ASGI (au lieu de WSGI) et nécessite un serveur compatible comme Daphne ou Uvicorn.

Question 23 : Expliquez le pattern Repository et Service Layer en Django

Le pattern Service Layer sépare la logique métier des vues et de l'ORM, facilitant les tests et la maintenance.

python
# services/article_service.py
from django.db import transaction

class ArticleService:
    """Service encapsulant la logique métier des articles."""

    @staticmethod
    def publish_article(article_id: int, user) -> Article:
        """Publie un article avec toutes les vérifications."""
        article = Article.objects.select_for_update().get(id=article_id)

        if article.author != user:
            raise PermissionError("Seul l'auteur peut publier cet article")
        if article.status == "published":
            raise ValueError("L'article est déjà publié")

        article.status = "published"
        article.published_at = timezone.now()
        article.save(update_fields=["status", "published_at"])

        # Effets de bord : notification, cache, analytics
        send_newsletter.delay(article.id)
        cache.delete("popular_articles_v1")

        return article

    @staticmethod
    @transaction.atomic
    def bulk_archive(article_ids: list[int], user) -> int:
        """Archive plusieurs articles dans une transaction."""
        updated = Article.objects.filter(
            id__in=article_ids,
            author=user,
            status="published"
        ).update(status="archived", archived_at=timezone.now())
        return updated

Le Service Layer est le point d'entrée pour toute logique métier. Les vues et serializers délèguent au service, jamais directement à l'ORM.

Question 24 : Comment gérer les variables d'environnement et la configuration multi-environnement ?

La gestion de la configuration suit le principe des 12-Factor Apps : séparation stricte entre configuration et code.

python
# settings/base.py
# Configuration partagée entre tous les environnements
import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()  # Charge le fichier .env

BASE_DIR = Path(__file__).resolve().parent.parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # Obligatoire, pas de valeur par défaut
DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("DB_NAME", "myapp"),
        "USER": os.environ.get("DB_USER", "postgres"),
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
    }
}

# settings/production.py
from .base import *

DEBUG = False
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000  # 1 an

Ne jamais commiter les secrets dans le code. Utiliser des variables d'environnement ou un gestionnaire de secrets (Vault, AWS Secrets Manager).

Question 25 : Comment déployer une application Django en production ?

Le déploiement production nécessite une checklist complète couvrant la sécurité, la performance et la fiabilité.

python
# Checklist de déploiement Django

# 1. Vérification avec la commande built-in
# python manage.py check --deploy

# 2. Configuration WSGI/ASGI pour la production
# gunicorn.conf.py
import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1  # Formule recommandée
worker_class = "gthread"           # Workers avec threads
threads = 4
max_requests = 1000                # Recycler les workers pour éviter les fuites mémoire
max_requests_jitter = 50
timeout = 30
accesslog = "-"                    # Logs sur stdout
errorlog = "-"
python
# docker-compose.yml (configuration typique)
# Services : web (gunicorn), db (postgres), redis (cache/celery), worker (celery)

# 3. Fichiers statiques
# python manage.py collectstatic --noinput
# Servir via nginx ou un CDN (WhiteNoise pour les cas simples)

# 4. Configuration nginx
# - Proxy vers gunicorn sur le port 8000
# - Servir /static/ et /media/ directement
# - Activer gzip, HTTP/2, et les en-têtes de sécurité

Exécuter python manage.py check --deploy avant chaque mise en production. Cette commande vérifie les paramètres de sécurité essentiels.

Conclusion

Ces 25 questions couvrent l'essentiel des entretiens Django et Python, des fondamentaux de l'architecture MVT aux patterns de déploiement production.

Checklist de préparation :

  • ✅ Architecture MVT et cycle de vie des requêtes
  • ✅ ORM : QuerySets, N+1, select_related, prefetch_related
  • ✅ Django REST Framework : serializers, pagination, permissions
  • ✅ Sécurité : CSRF, authentification, permissions
  • ✅ Performance : optimisation ORM, caching, indexes
  • ✅ Tests : TestCase, APITestCase, factories
  • ✅ Patterns avancés : Channels, Service Layer, déploiement
Aller plus loin

Chaque question mérite un approfondissement avec la documentation officielle de Django. Les recruteurs valorisent les candidats qui connaissent les subtilités du framework et savent justifier leurs choix techniques.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#django
#python
#interview
#django rest framework
#entretien technique

Partager

Articles similaires