Leçon 03-3 — Tuples et dictionnaires
1. Objectif
À la fin de cette leçon tu sauras : créer et déballer un tuple, expliquer ce qu'« immuable » veut dire, créer un dictionnaire, lire une valeur avec d[k] ou .get() (et choisir lequel), ajouter/modifier/supprimer des paires, parcourir un dict avec .items(), et implémenter le motif de comptage le plus utile de Python.
2. Pourquoi c'est important
La liste range des valeurs en ordre ; mais la moitié des données de la vraie vie sont des associations : un nom → un âge, un mot → son nombre d'occurrences, un identifiant → une fiche. Le dictionnaire est la deuxième structure la plus utilisée de Python — les fichiers JSON des APIs (⏩ niveau
- sont littéralement des dicts. Le tuple, lui, est partout en discret :
coordonnées, paires de enumerate et de .items(), retours multiples des fonctions (⏩ niveau 4). Tu utilises déjà des tuples sans le savoir depuis a, b = b, a (⏪ niveau 01).
3. Explication simple
Un tuple, c'est une liste scellée. On crée, on lit… et c'est tout : ni append, ni modification. Parfait pour ce qui va ensemble et ne change pas — un point GPS, une paire (nom, âge) :
point = (3, 5)
print(point[0]) # 3 : mêmes index que les listes
x, y = point # déballage : chaque variable reçoit son élément
Un dictionnaire, c'est un annuaire. Au lieu de casiers numérotés (0, 1, 2…), des casiers étiquetés par une clé de ton choix :
ages = {"Ada": 36, "Linus": 55}
print(ages["Ada"]) # 36 : on cherche par CLÉ, pas par position
ages["Guido"] = 69 # nouvelle paire clé → valeur
liste : position → valeur dict : clé → valeur
[0] → "Ada" "Ada" → 36
[1] → "Linus" "Linus" → 55
La question qui choisit la structure : « comment vais-je retrouver cette donnée ? » Par sa position → liste. Par un nom/étiquette → dict.
4. Explication approfondie
a) Immuable ne veut pas dire inutile. Un tuple ne PEUT pas être modifié (point[0] = 10 → TypeError). C'est une garantie : personne, nulle part dans le programme, ne modifiera ce point par accident — un tuple n'a aucun des pièges de mutabilité des listes (⏩ 03-4). C'est aussi ce qui autorise un tuple à servir de clé de dict (les listes en sont incapables — voir e). Règle pratique : collection homogène qui évolue → liste ; groupe hétérogène figé (un « enregistrement ») → tuple.
b) Le piège du tuple à un élément. Les parenthèses ne font pas le tuple, la virgule oui : (3) est juste l'entier 3 entre parenthèses ; le tuple à un élément s'écrit (3,). Et t = 1, 2, 3 (sans parenthèses) est un tuple valide — c'est exactement ce que fait a, b = b, a.
c) Le déballage exige le bon compte. x, y = (1, 2, 3) → ValueError: too many values to unpack. Le nombre de variables à gauche doit égaler le nombre d'éléments à droite. C'est le mécanisme derrière for i, name in enumerate(...) (⏪ 03-2) et for k, v in d.items() (exemple 5).
d) d[k] exige, .get(k) propose. Lire une clé absente avec d[k] lève KeyError ; .get(k) renvoie None, et .get(k, defaut) renvoie ton défaut. Les trois ont leur usage : d[k] quand l'absence est un BUG (tu veux planter tôt), .get(k, defaut) quand l'absence est un cas normal. Le motif roi du comptage : counts[w] = counts.get(w, 0) + 1 — « la valeur actuelle, ou 0 si première fois, plus un ».
e) Les clés doivent être immuables, les valeurs sont libres. Clés valides : str, int, tuple… ; interdites : listes et dicts (TypeError: cannot use 'list' as a dict key (unhashable type: 'list')). Chaque clé est unique : réassigner une clé existante écrase la valeur (pas de doublon possible). Depuis Python 3.7, un dict conserve l'ordre d'insertion — pratique pour l'affichage, mais si tu veux du trié, trie explicitement (sorted(d) sur les clés, sorted(d.items()) sur les paires).
Difficulté honnête : rien ici n'est conceptuellement dur, mais il y a beaucoup de vocabulaire (get, items, del, déballage…) — c'est une leçon de pratique. Compte 4–5 jours. La suivante (03-4) est la difficile ; arrive-s-y avec les dicts fluides.
5. Exemples commentés
Exemple 1 — tuples : créer, lire, déballer
point = (3, 5)
print(point[0], point[1]) # 3 5 : indexation comme une liste
print(len(point)) # 2
x, y = point # déballage : x=3, y=5
print(f"x={x}, y={y}") # x=3, y=5
single = (3,) # tuple à UN élément : la virgule obligatoire
not_a_tuple = (3) # ceci est un simple int !
print(type(single), type(not_a_tuple))
# <class 'tuple'> <class 'int'>
Exemple 2 — dict : créer, lire, écrire, supprimer
ages = {"Ada": 36, "Linus": 55}
print(ages["Ada"]) # 36
ages["Guido"] = 69 # AJOUTE une paire (clé nouvelle)
ages["Ada"] = 37 # MODIFIE (clé existante → écrase la valeur)
print(ages) # {'Ada': 37, 'Linus': 55, 'Guido': 69}
del ages["Linus"] # supprime la paire entière
print(ages) # {'Ada': 37, 'Guido': 69}
print(len(ages)) # 2 : len compte les paires
Même syntaxe d[k] = v pour ajouter et modifier : c'est la clé qui décide.
Exemple 3 — d[k] vs .get() vs in
ages = {"Ada": 37, "Guido": 69}
print(ages.get("Linus")) # None : absent, pas de crash
print(ages.get("Linus", 0)) # 0 : absent → MON défaut
print("Ada" in ages) # True : in teste les CLÉS
print("Linus" in ages) # False
if "Linus" in ages: # l'autre façon sûre de lire
print(ages["Linus"])
else:
print("Linus inconnu") # ← ce chemin s'exécute
Exemple 4 — parcourir un dict
ages = {"Ada": 37, "Guido": 69}
for name in ages: # parcours simple : les CLÉS
print(name) # Ada puis Guido
for name, age in ages.items(): # clés ET valeurs : .items() + déballage
print(f"{name} a {age} ans")
# Ada a 37 ans
# Guido a 69 ans
print(list(ages.keys())) # ['Ada', 'Guido']
print(list(ages.values())) # [37, 69]
print(list(ages.items())) # [('Ada', 37), ('Guido', 69)] ← des tuples !
.items() produit des paires — des tuples — que le for déballe. Boucle sur les clés seules : for k in d: suffit, pas besoin de .keys().
Exemple 5 — LE motif de comptage
sentence = "le chat dort et le chien dort"
counts = {}
for word in sentence.split(): # split() découpe la chaîne en
counts[word] = counts.get(word, 0) + 1 # liste de mots : ['le', 'chat', ...]
print(counts)
# {'le': 2, 'chat': 1, 'dort': 2, 'et': 1, 'chien': 1}
Déroule la première itération : counts.get("le", 0) → 0 (dict vide) → counts["le"] = 1. À la deuxième rencontre de "le" : get → 1 → counts["le"] = 2. Ce motif de trois lignes sert à compter N'IMPORTE QUOI ; tu le réutiliseras toute ta vie. (split() est une méthode de chaîne : elle découpe sur les espaces et renvoie une liste.)
Exemple 6 — un tuple comme clé de dict
treasure_map = {(0, 0): "depart", (2, 3): "tresor"}
position = (2, 3)
print(treasure_map[position]) # tresor
print(treasure_map.get((5, 5), "rien ici")) # rien ici
Impossible avec une liste comme clé — c'est l'immuabilité du tuple qui rend ça légal. Grilles de jeu, coordonnées, paires (jour, heure) : très courant.
6. Erreurs fréquentes
1) Modifier un tuple
point = (3, 5)
point[0] = 10 # TypeError: 'tuple' object does not support item assignment
C'est le contrat du tuple. Si la donnée doit changer, il fallait une liste ; sinon, crée un nouveau tuple : point = (10, point[1]).
2) Lire une clé absente avec d[k]
ages = {"Ada": 36}
print(ages["Linus"]) # KeyError: 'Linus'
Correction selon l'intention : ages.get("Linus", 0) si l'absence est normale, if "Linus" in ages: pour brancher, ou laisser planter si l'absence est un bug à découvrir tôt.
3) Oublier .items() dans le parcours à deux variables
ages = {"Ada": 36, "Linus": 55}
for name, age in ages: # ValueError: too many values to unpack (expected 2)
print(name, age)
Sans .items(), le for reçoit les clés et tente de déballer "Ada" en deux variables. Pire : si toutes les clés font exactement 2 caractères ({"ab": 1}), ça « marche » et affiche a b — un bug silencieux. Dès que tu écris deux variables dans un for sur un dict : .items().
4) Le tuple à un élément sans virgule
coords = (3) # int, pas tuple !
print(coords[0]) # TypeError: 'int' object is not subscriptable
(3,) avec la virgule. L'erreur n'explose qu'à l'utilisation, parfois loin de la création.
5) Une liste comme clé de dict
scores = {}
scores[["Ada", "Lovelace"]] = 18
# TypeError: cannot use 'list' as a dict key (unhashable type: 'list')
Les clés doivent être immuables. Correction : un tuple — scores[("Ada", "Lovelace")] = 18.
6) .get() sans défaut suivi d'un calcul
total = ages.get("Linus") + 1 # TypeError: unsupported operand type(s)
# for +: 'NoneType' and 'int'
get a renvoyé None sans bruit, le + 1 explose. Quand la valeur sert à un calcul, donne TOUJOURS un défaut : ages.get("Linus", 0) + 1.
7. Lire les messages d'erreur
KeyError — l'exception signature du dict :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\contacts.py", line 2, in <module>
print(ages["Linus"])
~~~~^^^^^^^^^
KeyError: 'Linus'
De bas en haut : KeyError: 'Linus' — le message est la clé coupable, entre quotes. Trois causes à vérifier dans l'ordre : (1) faute de frappe ou de casse ("linus" ≠ "Linus" — affiche list(d.keys()) pour comparer) ; (2) la clé n'a simplement pas encore été ajoutée (→ .get ou in) ; (3) la clé est du mauvais type (d[2] ne trouvera jamais la clé "2" — piège classique après un input()).
ValueError de déballage :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\contacts.py", line 1, in <module>
x, y = (1, 2, 3)
^^^^
ValueError: too many values to unpack (expected 2, got 3)
expected 2, got 3 : deux variables à gauche, trois valeurs à droite. La version not enough values to unpack (expected 3, got 2) est l'inverse. Dans un for sur un dict, ce message crie « tu as oublié .items() ».
TypeError du tuple :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\contacts.py", line 2, in <module>
point[0] = 10
~~~~~^^^
TypeError: 'tuple' object does not support item assignment
« item assignment » = affectation par index. L'objet visé par les ~~~~~ est immuable : tu voulais une liste, ou un nouveau tuple.
8. Exercices — faciles
Règles : un fichier par exercice dans
lessons/level-03-structures-donnees/exercices/mes-reponses/, nomméex-03-3-a.py, etc. Prédis chaque sortie AVANT d'exécuter.
a) Crée un tuple city = ("Nice", 43.7, 7.27) (nom, latitude, longitude). Déballe-le en trois variables et affiche Nice : lat 43.7, lon 7.27 en f-string. Puis essaie city[0] = "Lyon", lis le traceback, et note en commentaire ce qu'il signifie.
b) Crée un dict stock avec trois produits et leurs quantités. Fais dans l'ordre : affiche une quantité avec d[k] ; ajoute un produit ; modifie une quantité existante ; supprime un produit avec del ; affiche le dict final et son len.
c) Prédis les cinq sorties, puis vérifie :
d = {"a": 1, "b": 2}
print(d.get("a"))
print(d.get("z"))
print(d.get("z", 0))
print("z" in d)
print(2 in d)
(La dernière est piégeuse : que teste in exactement ?)
9. Exercices — moyens
a) Compte les lettres du mot "anticonstitutionnellement" avec le motif de l'exemple 5 (sans split : on parcourt directement la chaîne avec for). Affiche ensuite chaque lettre et son compte au format n : 5, une par ligne, triées par lettre (indice : sorted(counts.items())).
b) Ce programme doit afficher chaque prénom avec sa note, mais il est buggé. SANS l'exécuter : dis ce qu'il affiche réellement (surprise : il ne plante pas !), explique pourquoi, puis corrige :
grades = {"Bo": 15, "Al": 12}
for name, grade in grades:
print(name, grade)
Puis remplace les clés par {"Bob": 15, "Alice": 12} et prédis ce qui change.
c) Construis un annuaire : dict nom → tuple (ville, annee_naissance) avec trois personnes. Parcours-le avec .items() ET un déballage du tuple pour afficher Ada — Londres, 1815, une ligne par personne.
10. Exercices — difficiles
a) Inverser un dict. À partir de grades = {"Ada": 18, "Linus": 15, "Guido": 18}, construis le dict inversé by_grade : note → liste des prénoms ayant cette note. Attendu : {18: ['Ada', 'Guido'], 15: ['Linus']}. (La difficulté : deux prénoms partagent la note 18 — une simple inversion clé↔valeur en écraserait un.)
- Indice 1 : parcours
grades.items(). Pour chaque paire, la note devient la clé d'arrivée. Que faire si cette clé existe déjà dansby_grade? Et si elle n'existe pas encore ? - Indice 2 : deux cas → un
if grade in by_grade:…else:… - Indice 3 : si la note est déjà là,
by_grade[grade].append(name); sinon crée la liste :by_grade[grade] = [name].
11. Mini-projet lié — « compteur de mots »
Écris word_counter.py : demande une phrase avec input(), compte chaque mot (motif de l'exemple 5), puis affiche le rapport :
> le chat dort et le chien dort
5 mots differents, 7 mots au total
le : 2
chat : 1
dort : 2
et : 1
chien : 1
Contraintes : passe la phrase en minuscules d'abord (sentence.lower()) pour que Le et le comptent ensemble ; « mots au total » se calcule sur la liste de split(), « mots différents » sur le dict — explique la différence en commentaire. Durée cible : 20–30 min.
C'est le deuxième mini-projet annoncé du niveau, et l'embryon d'un vrai projet : la fiche projects/mini-projects/05-analyseur-de-texte.md en fait un analyseur complet (ponctuation, top 5, fréquence des lettres). Tu peux l'attaquer dès la fin de cette leçon — c'est le meilleur entraînement possible avant la 03-4.
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-03-3-solutions.md.
Méthode : (1) tous les exercices avant les solutions ; (2) compare le raisonnement ; (3) journal pour chaque écart ; (4) le motif d.get(k, 0) + 1 doit sortir de tes doigts sans réfléchir — si ce n'est pas le cas, refais 9-a et le mini-projet avec une autre phrase demain.
13. À retenir
- Tuple = liste scellée : indexation et déballage oui, modification non.
- Tuple à un élément :
(3,)— c'est la virgule qui fait le tuple. - Dict = clé → valeur ; on retrouve par clé, pas par position ; clés uniques ;
d[k] = vajoute OU écrase. d[k]plante si la clé manque (KeyError) ;d.get(k, defaut)jamais.insur un dict teste les clés.- Parcours :
for k in d:(clés),for k, v in d.items():(paires) — deux variables ⇒.items()obligatoire. - LE motif de comptage :
counts[x] = counts.get(x, 0) + 1. - Clés de dict : types immuables seulement (str, int, tuple) — jamais une liste.
14. Questions de révision
- Dans quels cas préfère-t-on un tuple à une liste ? Donne deux raisons.
- Pourquoi
(5)n'est-il pas un tuple, et comment écrire le tuple à un élément ? - Quelle est la différence de comportement entre
d["x"],d.get("x")etd.get("x", 0)quand la clé"x"est absente ? Quand choisir chacun ? - Que teste exactement
36 in ages? Comment tester la présence d'une valeur ? - Pourquoi
for k, v in d:est-il faux, et pourquoi peut-il parfois ne PAS planter (le pire des cas) ? - Récite le motif de comptage et explique le rôle du
0dansget(w, 0). - Pourquoi une liste ne peut-elle pas servir de clé de dict, et quel type utiliser à la place pour une clé composée ?
- Que signifie
ValueError: too many values to unpack (expected 2, got 3)et quelles sont ses deux causes classiques ?
15. Checklist de compréhension
- Je sais créer un tuple, le déballer, et provoquer volontairement le
TypeErrorde modification. - Je sais ajouter, modifier, supprimer et lire dans un dict sans modèle sous les yeux.
- Je sais choisir entre
d[k],.get(k, defaut)etif k in d:selon la situation — et le justifier. - Je peux écrire le motif de comptage de tête, et l'expliquer ligne par ligne.
- Je peux expliquer le bug silencieux de l'exercice 9-b.
- J'ai fait le mini-projet et
Le chat/le chatcomptent pareil.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante — la 03-4 suppose les dicts acquis.
16. Commit conseillé
git add lessons/level-03-structures-donnees/exercices/mes-reponses/
git commit -m "exercises: complete lesson 03-3 tuples and dicts"
Puis, si c'est ta fin de session : entrée de journal + git push.