Corrigé

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.

NSI · Terminale · Épreuve pratique 2026 · 28 sept. 2026

  • dictionnaire
  • récursivité
  • test
  • assertion
  • format JSON
Corrigé sujet 5 / Épreuve pratique NSI 2026 : dictionnaires imbriqués, récursivité et tests en Python

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.

Avant de commencer
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
def total_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épart
    for categorie in empreinte:                  # on parcourt les catégories une à une
        total = total + empreinte[categorie]     # et on ajoute l'empreinte de la catégorie
    return total

Reste la deuxième partie de la consigne : afficher le total d'Ada en utilisant les fonctions fournies pour accéder au fichier empreinte_ada_agr.json. En examinant empreinte.py, on remarque la présence, en début de fichier, du bloc :

Python
########### Fonctions données ###########
def chargement_json(nom_fichier):
    """Charge le contenu d'un fichier JSON dans un dictionnaire Python renvoyé"""
    with open(nom_fichier, "r", encoding="utf8") as curseur:
        return json.load(curseur)
def est_dictionnaire(objet):
    """Teste si un objet est de type dictionnaire"""
    return isinstance(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
def total_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épart
    for 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 total
        else:                                  # 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 :

Python
def test_total_rec():
    test_dico1 = {"a": 1, "d": 2}
    assert total_rec(test_dico1) == 3
    test_dico2 = {"a": {"b": 1, "c": 2}, "d": {"e": 3}}
    assert total_rec(test_dico2) == 6
    test_dico3 = {"Logement": {"Energie": {"Electricité": 206, "Cuisson": 105},
                               "Construction": 650},
                  "Transport": 14}       # trois niveaux, comme dans le fichier d'Ada
    assert total_rec(test_dico3) == 975  # 206 + 105 + 650 + 14

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
def alerte_valeur_aberrante(empreinte, limite):
    for categorie, valeur in empreinte.items():
        if est_dictionnaire(valeur):
            return alerte_valeur_aberrante(valeur, limite)
        else:
            if valeur > limite:
                return True
    return False

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
def alerte_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 ?
                return True                              # si oui : on s'arrête tout de suite
        else:
            if valeur > limite:                          # un nombre au-dessus de la limite ?
                return True
    return False

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 à 1500
print(alerte_valeur_aberrante(empreinte_brute, 1499))  # attendu : True, 1500 > 1499
print(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
def test_alerte():
    assert alerte_valeur_aberrante({}, 10) == False                       # vide
    assert alerte_valeur_aberrante({"a": 5, "b": 8}, 10) == False         # plat, rien au-dessus
    assert alerte_valeur_aberrante({"a": 5, "b": 20}, 10) == True         # plat, une valeur au-dessus
    assert alerte_valeur_aberrante({"a": 10}, 10) == False                # égal à la limite
    assert alerte_valeur_aberrante({"a": {"b": 20}}, 10) == True          # alerte dans une sous-catégorie
    assert alerte_valeur_aberrante({"a": {"b": 1}, "c": 20}, 10) == True  # le bug de la question 3
    assert alerte_valeur_aberrante({"a": {"b": {"c": 20}}}, 10) == True   # trois niveaux
    print("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.

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

Toutes les corrections des épreuves pratiques NSI

Si la récursivité reste floue malgré les exercices, un accompagnement régulier fait souvent plus qu'un sujet de plus.

Mis à jour le 06 oct. 2026