Corrigé sujet 5 / Épreuve pratique NSI 2026 : dictionnaires imbriqués, récursivité et tests en Python
Le sujet 5 de l'épreuve pratique NSI 2026 part de l'empreinte carbone d'Ada, rangée dans des dictionnaires imbriqués au format JSON. Une somme récursive à écrire, une fonction d'alerte qui s'arrête trop tôt, et le jeu de tests qui aurait attrapé le bug.
Ce sujet 5 de la session 2026 part d'un simulateur d'empreinte carbone : Ada a répondu aux questions du site nosgestesclimat.fr, et ses résultats sont rangés dans deux fichiers JSON, l'un agrégé en cinq catégories, l'autre détaillé poste par poste dans des dictionnaires imbriqués. Tu vas additionner un dictionnaire simple, puis un dictionnaire à plusieurs étages grâce à la récursivité, débusquer une erreur de conception dans une fonction d'alerte, et construire le jeu de tests qui l'aurait révélée. Une heure d'épreuve pratique, aussi appelée ECE de NSI, quatre questions et un fichier empreinte.py à compléter.
empreinte.py, empreinte_ada.json et empreinte_ada_agr.json dans le même dossier
Rien à installer, json est dans la bibliothèque standard de Python
Ouvrir empreinte.py dans ton éditeur Python
Question 1 / Somme d'un dictionnaire simple
Tu dois compléter la fonction total_simple, qui reçoit le dictionnaire agrégé et renvoie la somme de ses valeurs, puis afficher l'empreinte totale d'Ada en passant par les fonctions fournies pour lire le fichier.
Le fichier empreinte_ada_agr.json comporte cinq lignes seulement :
Catégorie
Empreinte (kg de CO2)
Logement
2660
Alimentation
1500
Transport
708
Consommation
893
Services sociétaux
1491
Un dictionnaire, ça se parcourt comme une liste, à une différence près : dans for categorie in empreinte, la variable categorie reçoit les clés, pas les valeurs. L'empreinte de la catégorie, c'est empreinte[categorie]. Le reste, c'est une somme : un total qui part de 0, et on ajoute.
Python
deftotal_simple(empreinte):
"""Fonction qui renvoie l'empreinte carbone totale d'un dictionnaire associant
une empreinte carbone à des noms de catégories"""
total = 0# le total, vide au départfor categorie in empreinte: # on parcourt les catégories une à une
total = total + empreinte[categorie] # et on ajoute l'empreinte de la catégoriereturn total
Reste la deuxième partie de la consigne : afficher le total d'Ada en utilisant les fonctions fournies pour accéder au fichierempreinte_ada_agr.json. En examinant empreinte.py, on remarque la présence, en début de fichier, du bloc :
Python
########### Fonctions données ###########defchargement_json(nom_fichier):
"""Charge le contenu d'un fichier JSON dans un dictionnaire Python renvoyé"""withopen(nom_fichier, "r", encoding="utf8") as curseur:
return json.load(curseur)
defest_dictionnaire(objet):
"""Teste si un objet est de type dictionnaire"""returnisinstance(objet, dict)
##########################################
Tu comprends alors qu'il va falloir utiliser la fonction chargement_json, qui ouvre le fichier et renvoie le dictionnaire.
Ainsi, pour tester ta fonction total_simple, tu écris juste :
Python
empreinte_agr = chargement_json("empreinte_ada_agr.json") # on lit le fichier agrégéprint(total_simple(empreinte_agr)) # et on affiche le total d'Ada
Calcule d'abord le résultat au brouillon : 2660 + 1500 = 4160, + 708 = 4868, + 893 = 5761, + 1491 = 7252. Et en lançant le test, tu obtiens :
Code
7252
Tu appelles le professeur, tu lui montres la fonction et ton test, en lui disant : « le total d'Ada est de 7252 kg, et je l'ai vérifié à la main ».
Les pièges classiques
Recopier le dictionnaire dans le code au lieu d'appeler chargement_json. Le résultat est le même, mais la consigne dit « en utilisant les fonctions fournies ».
Charger le mauvais fichier .json, c'est-à-dire empreinte_ada.json au lieu du fichier agrégé empreinte_ada_agr.json. En testant sur le mauvais fichier, tu risques d'avoir une erreur, alors que ton code est parfaitement correct !
Lancer le script depuis un autre dossier que celui des fichiers JSON : FileNotFoundError. Lis le message avant de retoucher ton code.
Question 2 / La somme récursive
L'énoncé te demande d'écrire total_rec, la même somme que total_simple mais sur le vrai fichier d'Ada, où une valeur peut être un nombre ou un dictionnaire qui détaille le poste. Puis de compléter les tests fournis par un cas qui ressemble à ce fichier.
Voilà à quoi ressemble la catégorie Logement dans empreinte_ada.json :
Clé
Valeur
Energie
un dictionnaire : Electricité 206, Cuisson 105, Chauffage individuel 1500
Construction
650
Location
37
Ameublement
162
Et Logement est elle-même une valeur du dictionnaire complet : des dictionnaires dans des dictionnaires, sur trois niveaux.
Qu'est-ce qui change par rapport à la question 1 ? Une seule chose : au moment d'ajouter une valeur, tu ne sais pas si c'est un nombre ou un dictionnaire. Alors on pose la question à la machine avec est_dictionnaire, et on se donne deux cas :
si la valeur est un nombre, on l'ajoute, comme avant ;
si c'est un dictionnaire, on ajoute... son total. Et le total d'un dictionnaire, c'est justement ce que total_rec calcule. Donc on rappelle total_rec sur ce sous-dictionnaire. C'est le fameux appel récursif !
Python
deftotal_rec(empreinte):
"""Fonction récursive qui renvoie l'empreinte carbone totale représentée
par un dictionnaire dont les valeurs peuvent aussi être des dictionnaires"""
total = 0# le total, vide au départfor categorie in empreinte: # on parcourt les clés
valeur = empreinte[categorie] # la valeur associée à la cléif est_dictionnaire(valeur): # une sous-catégorie : on recommence dedans
total = total + total_rec(valeur) # et on ajoute son totalelse: # un nombre : on l'ajoute directement
total = total + valeur
return total
Regarde bien la ligne total = total + total_rec(valeur) : l'appel récursif ne sert à rien tout seul, il faut récupérer ce qu'il renvoie et l'ajouter au total.
Reste le test. test_total_rec est fournie avec deux dictionnaires "jouets", et l'énoncé te demande un cas « plus proche de la structure » du fichier d'Ada. Prends la catégorie Logement et allège-la, pour que la somme se calcule de tête :
Et le sujet t'offre un test sans le dire : le fichier agrégé et le fichier brut décrivent la même Ada, les deux totaux doivent donc coïncider.
Python
test_total_rec() # aucune erreur : les trois assert passent
empreinte_brute = chargement_json("empreinte_ada.json") # on lit le fichier détailléprint(total_rec(empreinte_brute)) # attendu : le 7252 de la question 1
Code
7252
Le même 7252 qu'à la question 1, par un chemin complètement différent. Au professeur, tu montres les deux totaux côte à côte et ton test_dico3, et tu lui dis : « la fonction s'appelle elle-même dès qu'une valeur est un dictionnaire, et je retrouve bien le total de la question 1 ».
Les pièges classiques
Écrire total_rec(valeur) sur une ligne toute seule, sans l'ajouter au total. La récursion a lieu, mais le résultat est perdu, et la fonction ne compte que les nombres du premier niveau. Affiche ce qu'elle renvoie sur test_dico2 avant de chercher plus loin.
Oublier le return total. La fonction renvoie None, et total + None plante dans l'appel du dessus : le message d'erreur pointe une ligne qui est juste.
Oublier de lancer test_total_rec(). Elle est fournie, mais aucune ligne du fichier ne l'appelle.
Oublier de compléter test_total_rec(). Sans le cas « plus proche de la structure », la consigne n'est qu'à moitié traitée.
Question 3 / Le bug de l'alerte
On te donne alerte_valeur_aberrante, censée renvoyer True dès qu'une valeur, sous-catégories comprises, dépasse strictement la limite. Sur le fichier d'Ada avec une limite de 1000, elle renvoie False alors que le chauffage vaut 1500. Tu dois expliquer l'origine de l'erreur, puis proposer une version corrigée.
Voici la fonction fournie :
Python
defalerte_valeur_aberrante(empreinte, limite):
for categorie, valeur in empreinte.items():
if est_dictionnaire(valeur):
return alerte_valeur_aberrante(valeur, limite)
else:
if valeur > limite:
returnTruereturnFalse
Premier réflexe, avant de relire le code ligne par ligne : on affiche ce qu'il fait vraiment. Un print(categorie) juste sous le for, et on lance sur le fichier d'Ada :
Code
Alimentation
Repas
Viande
Blanche
Rouge
False
Cinq clés visitées sur une trentaine, et Logement n'apparaît jamais. La fonction ne se trompe pas dans sa comparaison : elle s'arrête trop tôt.
Comment faire ?
Pourquoi s'arrête-t-elle ? Regarde la ligne return alerte_valeur_aberrante(valeur, limite). Un return dans une boucle, c'est une sortie forcée : dès qu'il s'exécute, la fonction rend sa réponse et la boucle est abandonnée. Ici, dès la première sous-catégorie rencontrée, la fonction renvoie quoi qu'il arrive le résultat de son exploration. Sans alerte dedans, c'est False, et les clés suivantes ne sont jamais regardées.
Prends le contre-exemple le plus simple : {"a": {"b": 1}, "c": 50} avec une limite de 10. La valeur associée à la clé "a" est un dictionnaire, la fonction renvoie alerte_valeur_aberrante({"b": 1}, 10), c'est-à-dire False... et la valeur 50 associée à la clé "c" n'est jamais lue. Sur le fichier d'Ada, c'est pareil en plus long : False remonte de Viande jusqu'au résultat final, et Logement était la clé suivante.
Mais là, tu n'es qu'à la moitié de la question : l'énoncé demande d'expliquer précisément. Je t'encourage à écrire des mots-clés au brouillon : « return » et « première sous-catégorie », ce sont ceux que tu diras au professeur.
La correction tient dans la ligne fautive. Le résultat de l'appel récursif ne doit faire sortir de la fonction que s'il vaut True. S'il vaut False, la boucle continue :
Python
defalerte_valeur_aberrante(empreinte, limite):
"""
Fonction censée déterminer si au moins une valeur du dictionnaire
dépasse strictement la limite donnée.
"""for categorie, valeur in empreinte.items():
if est_dictionnaire(valeur):
if alerte_valeur_aberrante(valeur, limite): # une alerte dans la sous-catégorie ?returnTrue# si oui : on s'arrête tout de suiteelse:
if valeur > limite: # un nombre au-dessus de la limite ?returnTruereturnFalse
Rien d'autre ne bouge : corrige l'erreur, pas la fonction. Et bien sûr... on teste son code ! Le cas de l'énoncé, puis les deux bornes autour de la plus grosse valeur du fichier, le chauffage à 1500 :
Python
empreinte_brute = chargement_json("empreinte_ada.json")
print(alerte_valeur_aberrante(empreinte_brute, 1000)) # attendu : True, le chauffage à 1500print(alerte_valeur_aberrante(empreinte_brute, 1499)) # attendu : True, 1500 > 1499print(alerte_valeur_aberrante(empreinte_brute, 1500)) # attendu : False, 1500 n'est pas strictement supérieur à 1500
Code
True
True
False
Au professeur, tu montres la sortie du print(categorie) sur la version fautive, et tu lui dis : « le return devant l'appel récursif fait sortir de la boucle dès la première sous-catégorie, même sans alerte dedans ; maintenant je ne renvoie True que si elle en contient une ».
Les pièges classiques
Chercher le bug en relisant le code, sans rien afficher. Un print dans la boucle montre en dix secondes que Logement n'est jamais visité, ce qu'une relecture ne montre pas.
Suspecter la comparaison. > ou >=, c'est la fausse piste classique : 1500 est bien strictement supérieur à 1000, et changer le signe ne fait pas visiter plus de clés.
Réécrire la fonction autrement, par exemple en calculant le maximum. La question demande d'abord une explication, et une nouvelle fonction n'explique rien.
Question 4 / Un jeu de tests robuste
Tu dois proposer un jeu de tests pour la fonction corrigée : pour chacun, le dictionnaire d'entrée, le résultat attendu, et ce qu'il vérifie dans l'algorithme.
Le mot pivot de la consigne, c'est « pertinent ». Pas forcément beaucoup de tests, mais les bons : un test est pertinent quand il isole un seul comportement, et quand une version fausse le ferait échouer. Tu viens justement d'en croiser une : le meilleur test du jeu, c'est celui qui aurait révélé le bug de la question 3.
Qu'est-ce qui pourrait faire échouer une fonction qui parcourt un dictionnaire imbriqué et compare à un seuil ? On dresse le tableau, avec de tout petits dictionnaires et une limite de 10, pour pouvoir calculer de tête :
Dictionnaire
Limite
Attendu
Ce qu'on vérifie
{}
10
False
le dictionnaire vide : aucun tour de boucle, on arrive au return False
{"a": 5, "b": 8}
10
False
dictionnaire plat, aucune valeur au-dessus de la limite
{"a": 5, "b": 20}
10
True
dictionnaire plat, une valeur au-dessus de la limite
{"a": 10}
10
False
valeur égale à la limite, donc ça passe !
{"a": {"b": 20}}
10
True
l'alerte est dans une sous-catégorie, l'appel récursif remonte bien True
{"a": {"b": 1}, "c": 20}
10
True
sous-catégorie sans alerte avant la valeur aberrante : le bug de la question 3
{"a": {"b": {"c": 20}}}
10
True
trois niveaux d'imbrication
Regarde l'avant-dernier : sur la fonction d'origine, il est le seul à échouer. C'est lui qui garantit qu'on ne réintroduira jamais ce bug, et c'est ça que l'énoncé appelle « régression ». Les six autres vérifient que la correction n'a rien cassé : on repasse les anciens tests (qui passaient avant correction) pour vérifier que la correction appliquée ne casse pas ce qui marchait déjà.
En code, chaque ligne du tableau devient un assert :
Python
deftest_alerte():
assert alerte_valeur_aberrante({}, 10) == False# videassert alerte_valeur_aberrante({"a": 5, "b": 8}, 10) == False# plat, rien au-dessusassert alerte_valeur_aberrante({"a": 5, "b": 20}, 10) == True# plat, une valeur au-dessusassert alerte_valeur_aberrante({"a": 10}, 10) == False# égal à la limiteassert alerte_valeur_aberrante({"a": {"b": 20}}, 10) == True# alerte dans une sous-catégorieassert alerte_valeur_aberrante({"a": {"b": 1}, "c": 20}, 10) == True# le bug de la question 3assert alerte_valeur_aberrante({"a": {"b": {"c": 20}}}, 10) == True# trois niveauxprint("tous les tests passent")
On teste sur la version corrigée... tout passe :
Code
tous les tests passent
Au professeur, tu montres le tableau, tu lui dis que chaque ligne teste un comportement et un seul, et tu lui désignes celle qui aurait révélé l'erreur de la question 3.
Les pièges classiques
Ne proposer que des cas qui renvoient True. Une fonction qui renverrait True tout le temps les passerait tous.
Oublier le cas du bug de la question 3. C'est le seul test qui distingue la version fausse de la version corrigée, donc le seul qui protège d'une régression.
Tester uniquement sur le fichier d'Ada. Un dictionnaire de trente valeurs, c'est trop gros pour des tests... il faut des tests sur des dictionnaires de petite taille, pour savoir à l'avance ce que la fonction est censée retourner.
Récapitulatif des notions
Parcours d'un dictionnaire par ses clés, accès aux valeurs par empreinte[categorie]
Lecture d'un fichier JSON avec le module json
Récursivité sur des dictionnaires imbriqués : cas d'arrêt, appel sur le sous-dictionnaire, remontée du résultat
Effet d'un return placé dans une boucle
Jeu de tests avec assert : cas de base, cas limite, cas de non-régression
Deux corrections voisines travaillent les mêmes notions : le sujet 2, dictionnaires et fonction fournie à corriger, et le sujet 1, fonction de test à compléter avec assert.