Corrigé

Corrigé sujet 15 / Épreuve pratique NSI 2026 : chaînes de caractères, tests, requêtes SQL et dictionnaires

Le sujet 15 de l'épreuve pratique NSI 2026 se passe dans un cabinet vétérinaire qui veut rappeler par SMS la vaccination annuelle des chats. Des numéros de téléphone à nettoyer, une requête SQL sur trois tables, et une fonction de dictionnaire qui garde la mauvaise date.

NSI · Terminale · Épreuve pratique 2026 · 05 oct. 2026

  • assertion
  • chaîne de caractères
  • test
  • base de données
  • SQL
  • dictionnaire
Corrigé sujet 15 / Épreuve pratique NSI 2026 : chaînes de caractères, tests, requêtes SQL et dictionnaires

Ce sujet 15 de la session 2026 se déroule dans un cabinet vétérinaire : le vétérinaire veut envoyer par SMS un rappel de vaccination aux propriétaires de chats, et il faut préparer les données. Tu vas nettoyer des numéros de téléphone caractère par caractère, construire un jeu de tests, écrire une requête SQL qui relie trois tables, puis réparer une fonction qui range des consultations dans un dictionnaire. Une heure d'épreuve pratique, aussi appelée ECE de NSI, pour quatre questions, un fichier veto.py à compléter et une base de données cabinet.sqlite.

Avant de commencer
veto.py et cabinet.sqlite dans le même dossier
Rien à installer : sqlite3 fait partie de la bibliothèque standard de Python
Ouvrir veto.py dans ton éditeur Python

Question 1 / Nettoyer les numéros

Tu dois écrire la fonction normalisation_tel, qui reçoit un numéro de téléphone tapé n'importe comment et renvoie une chaîne qui ne contient plus que ses chiffres.

La fonction test_normalisation_tel fournie montre à quoi peuvent ressembler les numéros enregistrés, et surtout, comment ils doivent être normalisés :

Numéro enregistré Numéro attendu après normalisation
"06 12 99 90 12" "0612999012"
"02.12.99.90.12" "0212999012"
"0.6.12.99.90.12" "0612999012"
"06-12-99-90-12" "0612999012"
"(0)6.12.99-90-12 gilbert" "0612999012"

Des espaces, des points, des tirets, des parenthèses... et même un prénom !

La première idée que l'on peut avoir, c'est d'enlever les caractères en trop, un par un : espace, point, tiret... Ça marche... mais uniquement pour les numéros qui ne contiennent que des espaces, des points et des tirets. Sur le dernier exemple, il resterait "(0)612999012gilbert".

On va donc renverser le problème et faire la liste de ce qu'on garde : les chiffres, et rien d'autre. L'énoncé te donne d'ailleurs l'outil juste avant la question : la méthode isdigit, qui renvoie True quand un caractère est un chiffre.

Python
def normalisation_tel(tel):
    nettoye = ""                            # le numéro nettoyé, vide au départ
    for caractere in tel:                   # on parcourt le numéro, caractère par caractère
        if caractere.isdigit():             # si c'est un chiffre...
            nettoye = nettoye + caractere   # ... on le garde et on le met à la suite du "numéro nettoyé"
    return nettoye

Remarques :

  1. On part d'une chaîne vide, et on l'allonge d'un caractère à chaque chiffre rencontré. C'est un accumulateur, version chaîne de caractères.
  2. Le return est aligné avec le for : on renvoie le résultat une fois que tout le numéro a été parcouru.

Et comme toujours... on doit tester son code ! Le fichier fournit pour cela la fonction test_normalisation_tel, mais ne l'appelle nulle part. Tu ajoutes donc l'appel juste en dessous de la fonction de test :

Python
test_normalisation_tel()

Et quand tu exécutes tout ça :

Code
Les tests de la fonction normalisation_tel sont passés

Tu appelles le professeur, et tu lui montres ce message en lui disant : « je parcours le numéro et je ne garde que les chiffres, grâce à isdigit ; les huit tests fournis passent. »

Les pièges classiques

  • Écrire la fonction sans jamais lancer le test. Comme veto.py n'appelle pas test_normalisation_tel, l'exécution n'affiche rien du tout, et tu crois que tout va bien alors que ta fonction n'a jamais été testée ! Et en plus, l'énoncé parle de la fonction test_normalisation_tel, mais à aucun moment il ne dit que tu dois l'utiliser... on teste tes réflexes de développeur !
  • Renvoyer un nombre au lieu d'une chaîne. Avec int(nettoye), le 0 du début disparaît : "0612999012" devient 612999012, et le test échoue alors que tu as bien gardé les bons chiffres !
  • Indenter le return dans la boucle. La fonction s'arrête au premier caractère et renvoie "0", et le test échoue sans te dire pourquoi.

Question 2 / Tester la validation

Comment savoir qu'une fonction marche, quand ce n'est pas toi qui l'as écrite ? Ici, on te demande d'écrire un jeu de tests pour validation_tel, déjà fournie, qui dit si un numéro nettoyé est un numéro de portable valide.

La règle à vérifier est au début de l'énoncé :

La plateforme d'envoi de SMS requiert un numéro de téléphone au format 06XXXXXXXX ou 07XXXXXXXXX, 10 chiffres au total, sans espace, ni point, ni autre caractère.

Attention ! Dans l'énoncé, le format en 07 compte un X de trop : la suite de la phrase dit bien 10 chiffres au total.

Qu'est-ce qui pourrait bien rendre un numéro invalide ? Si on examine le code de validation_tel, on peut en avoir une petite idée : il y a trois if, donc trois façons de renvoyer False :

  1. Le numéro n'a pas 10 caractères,
  2. ou il ne commence pas par 0,
  3. ou son deuxième chiffre n'est ni un 6 ni un 7.

Un bon jeu de tests vérifie chaque condition séparément, avec un numéro qui n'est faux que pour une seule raison. Prends "12345" : il est trop court, et il ne commence pas par 0. Si la fonction oubliait de vérifier le 0, ce test passerait quand même, grâce à la longueur.

Il faut aussi des numéros valides, un en 06 et un en 07 : une fonction qui renverrait toujours False passerait tous les tests de numéros invalides ! Pour la longueur, on teste juste autour de 10 : 9 et 11 chiffres.

On peut donc dresser ce tableau :

Numéro testé Ce qu'on vérifie Attendu
"0612345678" 10 chiffres, commence par 06 True
"0712345678" 10 chiffres, commence par 07 True
"061234567" Commence bien par "06" mais un chiffre en moins (9 chiffres) False
"06123456789" Commence bien par "06" mais un chiffre en plus (11 chiffres) False
"6612345678" 10 chiffres, mais ne commence pas par '0' False
"0212345678" 10 chiffres, commence bien par '0', mais "02" : un téléphone fixe False

Je t'encourage, en situation d'examen, à remplir ce tableau au brouillon avant de coder.

Python
def test_validation_tel():
    assert validation_tel("0612345678") == True    # portable en 06
    assert validation_tel("0712345678") == True    # portable en 07
    assert validation_tel("061234567") == False    # 9 chiffres : un de moins
    assert validation_tel("06123456789") == False  # 11 chiffres : un de plus
    assert validation_tel("6612345678") == False   # ne commence pas par 0
    assert validation_tel("0212345678") == False   # commence par 02 : un fixe
    print('Les tests de la fonction validation_tel sont passés')

Le commentaire de chaque assertion dit ce qu'elle vérifie : si une ligne échoue, tu sais tout de suite quelle condition est en cause.

Et évidemment... on teste son code ! Tu appelles test_validation_tel() sous la fonction et tu obtiens :

Code
Les tests de la fonction validation_tel sont passés

À l'appel professeur, tu lui montres ton tableau et ce message, en lui disant : « j'ai testé avec deux numéros valides, en 06 et en 07, et quatre numéros non valides pour une seule raison chacun : validation_tel passe tous les tests. »

Les pièges classiques

  • Ne tester que des numéros valides. Une fonction qui renvoie toujours True passerait ton jeu de tests.
  • Tester des numéros faux pour plusieurs raisons à la fois. S'il échoue, on ne peut pas savoir pour quelle raison précisément.
  • Écrire la valeur attendue après avoir lancé la fonction. Si tu recopies ce que validation_tel renvoie, ton test confirme ce que fait la fonction, même quand elle se trompe.

Question 3 / Requête sur trois tables

On te donne la fonction proprietaires_animaux_nes_apres(date) en exemple, et l'énoncé te demande de t'en inspirer pour écrire consultation_vaccination_chat(date).

Cette fonction doit récupérer les consultations. Mais pas toutes les consultations - non. Il ne faut garder qu'une partie des consultations : les vaccinations, de chats, dont la date est postérieure à date. Pour chacune, on veut quatre champs :

Champ demandé table.colonne
id de l'animal animal.id
nom de l'animal animal.nom
téléphone du propriétaire proprietaire.telephone
date de la consultation consultation.date

Et la liste finale doit être triée par id d'animal, puis par date.

Quand tu ouvres la fonction proprietaires_animaux_nes_apres(date), tu vois qu'elle utilise cette requête :

SQL
SELECT proprietaire.nom, proprietaire.prenom
FROM proprietaire
    JOIN  animal ON proprietaire.id = animal.id_proprietaire
WHERE animal.date_naissance > ?
ORDER BY proprietaire.nom, proprietaire.prenom;

Tu comprends alors que tout le modèle est là : colonnes préfixées par leur table, JOIN pour relier deux tables, ? pour la date, ORDER BY sur deux champs. Le reste de la fonction se recopie tel quel.

Sélectionner les bonnes données, depuis les bonnes tables

Tu dois commencer par un SELECT qui récupère les champs dont tu as besoin, sélectionnant les table.colonne où ils se trouvent :

SQL
SELECT animal.id, animal.nom, proprietaire.telephone, consultation.date

Remarque : chaque colonne est préfixée par le nom de sa table. id existe dans les trois tables et nom dans deux : sans préfixe, SQLite ne sait pas laquelle tu veux.

Comment relier les tables entre elles ?

Tu sais déjà que tu vas avoir besoin des tables : animal, proprietaire et consultation.

Mais qu'est-ce qui relie une consultation à un numéro de téléphone ? Rien, directement : la table consultation ne connaît que id_animal. Il faut passer par l'animal, qui connaît id_proprietaire, puis par le propriétaire, qui connaît le téléphone. Ce sont les deux flèches du schéma de la base de données, qui était en page 2 de l'énoncé, et c'est le rôle de la jointure ! On écrit une condition ON par "flèche" :

  • ON consultation.id_animal = animal.id
  • ON animal.id_proprietaire = proprietaire.id

Donc on écrit :

SQL
FROM consultation
    JOIN animal ON consultation.id_animal = animal.id
    JOIN proprietaire ON animal.id_proprietaire = proprietaire.id

Filtrer par les conditions imposées

Les trois conditions de l'énoncé doivent être reliées par AND : une consultation n'est gardée que si elle les vérifie toutes les trois.

SQL
WHERE animal.espece = 'chat'
    AND consultation.motif = 'vaccination'
    AND consultation.date > ?

Les valeurs désirées 'chat' et 'vaccination' s'écrivent directement dans la requête, entre apostrophes. Seule la date change d'un appel à l'autre : elle passera par le ? et le tuple (date,) (comme dans la fonction exemple).

Dans le bon ordre

Enfin, l'énoncé impose l'ordre de tri des données :

SQL
ORDER BY animal.id, consultation.date

Et pour terminer, la requête complète

Elle doit être insérée dans une chaîne de caractères, comme dans la fonction exemple. Tu peux garder le ; final pour fermer proprement la requête.

Python
def consultation_vaccination_chat(date):
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    resultat = cursor.execute(
        """
    SELECT animal.id, animal.nom, proprietaire.telephone, consultation.date
    FROM consultation
        JOIN animal ON consultation.id_animal = animal.id
        JOIN proprietaire ON animal.id_proprietaire = proprietaire.id
    WHERE animal.espece = 'chat'
        AND consultation.motif = 'vaccination'
        AND consultation.date > ?
    ORDER BY animal.id, consultation.date;
    """,
        (date,),
    )
    return list(resultat)

L'énoncé exige que ta fonction passe les tests de test_consultation_vaccination_chat(). Cette fonction est déjà "appelée" en bas de la section, mais cet appel est commenté : tu enlèves le # pour que l'appel soit effectif, tu lances :

Code
Les tests de la fonction consultation_vaccination_chat sont passés

Lorsque tu appelles le professeur, tu lui montres la requête et ce message, en lui expliquant : « je relie consultation à animal, puis à proprietaire, par deux jointures ; je filtre sur l'espèce, le motif et la date, et je trie par animal puis par date. »

Les pièges classiques

  • Oublier une des trois conditions. Sans la condition sur motif, la fonction renvoie 163 consultations ; sans celle sur espece, 372. Le test échoue dès la première assertion, celle qui compte 118 lignes : relis alors ton WHERE avant le reste.
  • Écrire une colonne sans le nom de sa table. Avec SELECT id, nom, SQLite s'arrête sur ambiguous column name, alors que ta requête a l'air juste !
  • Écrire (date) au lieu de (date,). Sans la virgule, ce n'est pas un tuple : SQLite compte les 8 caractères de la date comme 8 paramètres, et tu obtiens Incorrect number of bindings supplied.
  • Lancer le fichier depuis un autre dossier. cabinet.sqlite n'est pas trouvé, sqlite3 crée une base vide à la place, et tu obtiens no such table alors que ton code est parfaitement correct !

Question 4 / Réparer la dernière vaccination

L'énoncé te demande d'abord d'expliquer le problème de derniere_vaccination, puis de la corriger pour que test_derniere_vaccination passe.

Cette question ne dépend pas de la base : le test travaille sur une liste écrite à la main. Si tu n'as pas trouvé la réponse à la question 3, tu peux quand même traiter la question 4.

Tu lances le test : AssertionError, et rien d'autre. L'assertion te dit que les deux dictionnaires sont différents, sans te dire où. Il faut donc aller voir et comprendre ce que la fonction renvoie, et le test t'en donne les moyens : deux lignes en commentaire, juste au-dessus de l'assertion. Tu les décommentes :

Python
    for a,r in resultat.items():
        print(a, ":", r)
Code
16 : (16, 'Plume', '0.6.36.96.89.83', '20241024')
17 : (17, 'Gollum', '0.6.36.96.89.83', '20250113')
26 : (26, 'Olympe', '(0)4 73 98 01 23', '20250109')
32 : (32, 'Chopin', '06.37.97.66.64', '20241201')
34 : (34, 'Jazz', '0.6.37.51.65.52', '20250801')
35 : (35, 'Tango', '0324182', '20250706')
38 : (38, 'Loulou', '05-35-95-87-54', '20250209')
39 : (39, 'Tango', '07.45.48.02.42', '20250329')
40 : (40, 'Sésame', '07.45.48.02.42', '20250228')
41 : (41, 'Pixel', '0130709285', '20241204')

Regarde Plume, l'animal 16 : la fonction a gardé 20241024. Or Plume a deux consultations, le 24 octobre 2024 et le 25 novembre 2025, et la dernière est celle de 2025. Même chose pour Chopin (32) et Pixel (41) : la fonction fournie garde la plus ancienne date, au lieu de la plus récente.

La ligne qui décide du remplacement est celle-ci :

Python
        elif date < derniere[id_animal][3]:

En français : "si la date de cette consultation est plus petite que celle déjà rangée, on remplace". Les dates sont des chaînes au format AAAAMMJJ, comparées dans l'ordre alphabétique, caractère par caractère. Pour "20251125" et "20241024", les trois premiers caractères sont égaux, puis 5 est plus grand que 4 : "20251125" est donc plus grande. Avec ce format, plus petite veut dire plus ancienne.

On peut suivre Plume pas à pas :

Consultation lue Rangée avant Comparaison Rangée après
20241024 rien premier passage : on range 20241024
20251125 20241024 "20251125" < "20241024" : faux, on ne remplace pas 20241024

La fonction ne remplace la consultation rangée que par une plus ancienne : elle fait l'inverse de ce qu'on lui demande. La correction tient en un caractère : on remplace < par >.

Python
def derniere_vaccination(consultations):
    derniere = {}
    for consult in consultations:
        id_animal = consult[0]
        date = consult[3]
        if id_animal not in derniere:        # première consultation de cet animal : on la range
            derniere[id_animal] = consult
        elif date > derniere[id_animal][3]:  # plus récente que celle rangée : on remplace
            derniere[id_animal] = consult
    return derniere

Tu relances test_derniere_vaccination() pour voir :

Code
Les tests de la fonction derniere_vaccination sont passés

Quand le professeur vient te voir, tu lui montres l'affichage de Plume, puis ce message, en lui disant : « avec <, la fonction remplaçait par une date plus petite, donc plus ancienne ; avec >, elle garde la plus récente, et le test passe. »

Les pièges classiques

  • Chercher l'erreur sans avoir affiché le résultat. Sans l'affichage du dictionnaire obtenu, tu relis la fonction ligne à ligne sans savoir quoi chercher.
  • Modifier le test au lieu de la fonction. Remplacer 20251125 par 20241024 dans le dictionnaire attendu fait passer le test, mais la fonction garde toujours la mauvaise date.
  • Corriger sans expliquer. La consigne demande d'abord d'expliquer le problème. Si tu ne fais que corriger le problème sans expliquer pourquoi c'était précisément cette correction, tu n'as que la moitié des points.

Récapitulatif des notions

  • Parcours d'une chaîne de caractères, accumulateur, méthode isdigit.
  • Jeu de tests : cas valides et invalides, une raison d'échec par cas, valeurs autour de la borne.
  • Requête SQL sur trois tables : JOIN ... ON, WHERE avec AND, ORDER BY sur deux champs, paramètre ?.
  • Comparaison de dates AAAAMMJJ dans l'ordre alphabétique.
  • Dictionnaire indexé par identifiant, débogage par affichage.

Les mêmes notions dans d'autres corrections :

Sujet issu de la banque officielle des épreuves pratiques NSI.

Toutes les corrections des épreuves pratiques NSI

Si tu veux t'entraîner sur ces sujets avec un prof qui les corrige toute l'année, on peut en parler.

Mis à jour le 06 oct. 2026