Leçon 03-1 — Listes : stocker en ordre

1. Objectif

À la fin de cette leçon tu sauras : créer une liste, lire et modifier ses éléments par index (positif et négatif), ajouter (append, insert) et retirer (remove, pop) des éléments, trier (sort / sorted), tester la présence d'une valeur avec in, et lire un IndexError sans paniquer.

2. Pourquoi c'est important

Jusqu'ici tes programmes manipulaient quelques variables : un score, un prix, un compteur. Les vrais programmes manipulent des quantités de données : les 200 lignes d'un relevé bancaire, les tâches d'une todo-list, les mots d'un texte. La liste est LA structure de base pour ça — c'est l'objet le plus utilisé de tout Python. Chaque niveau suivant (fonctions, fichiers, POO, APIs) te fera lire et produire des listes en permanence.

3. Explication simple

Une liste est une rangée de casiers numérotés, dans l'ordre, à partir de 0.

scores = [12, 7, 19]
index :     0    1    2
          ┌────┬────┬────┐
scores →  │ 12 │  7 │ 19 │
          └────┴────┴────┘

C'est tout : une liste, c'est de l'ordre + des numéros + le droit de modifier.

4. Explication approfondie

a) Les index négatifs comptent depuis la fin. scores[-1] est le dernier élément, scores[-2] l'avant-dernier. C'est du vrai Python idiomatique, pas une astuce : scores[-1] est plus lisible et plus sûr que scores[len(scores) - 1].

index positif :    0    1    2
index négatif :   -3   -2   -1
                ┌────┬────┬────┐
                │ 12 │  7 │ 19 │
                └────┴────┴────┘

b) Deux familles d'opérations — LA distinction du niveau. Certaines méthodes modifient la liste sur place et renvoient None : append, insert, remove, sort, pop (qui, lui, renvoie l'élément retiré). D'autres fabriquent une nouvelle valeur sans toucher la liste : sorted(), len(), sum(). Confondre les deux produit le bug classique lst = lst.sort() (section 6). À chaque méthode que tu apprends, pose-toi LA question : elle modifie, ou elle renvoie ?

c) Une liste peut tout contenir — mais ne devrait pas. [1, "deux", 3.0] est légal. En pratique, une bonne liste contient des éléments du même type : une liste de scores, une liste de noms. Les mélanges rendent le tri impossible (TypeError, section 7) et le code illisible. Quand tu voudras grouper des choses de types différents (un nom ET un âge), le tuple et le dictionnaire (⏩ leçon 03-3) seront les bons outils.

d) Une liste est un objet mutable. « Mutable » = modifiable après création. C'est sa force… et la source du piège le plus important du niveau : deux variables peuvent pointer vers la même liste, et la modifier par l'une « change » l'autre. ⏩ C'est toute la leçon 03-4 — la plus difficile du niveau. Pour l'instant, retiens juste : une liste peut être modifiée, un nombre ou un texte non.

Cette leçon-ci est surtout de la mécanique : beaucoup de méthodes à pratiquer, rien de conceptuellement dur. Compte 3–4 jours avec les exercices.

5. Exemples commentés

Exemple 1 — créer, lire, mesurer

scores = [12, 7, 19]
print(scores[0])       # 12   : premier casier
print(scores[2])       # 19   : troisième casier (index 2 !)
print(scores[-1])      # 19   : dernier, compté depuis la fin
print(len(scores))     # 3    : nombre d'éléments
print(scores)          # [12, 7, 19] : print affiche la liste entière

Exemple 2 — modifier un casier existant

scores = [12, 7, 19]
scores[1] = 8          # on REMPLACE le contenu du casier 1
print(scores)          # [12, 8, 19]

Exemple 3 — ajouter : append (au bout) et insert (où tu veux)

scores = [12, 8, 19]
scores.append(15)      # ajoute AU BOUT
print(scores)          # [12, 8, 19, 15]
scores.insert(0, 3)    # insère 3 À l'index 0 ; tout le reste se décale
print(scores)          # [3, 12, 8, 19, 15]

Exemple 4 — retirer : remove (par valeur) et pop (par index)

scores = [3, 12, 8, 19, 15]
scores.remove(19)      # retire la VALEUR 19 (la première rencontrée)
print(scores)          # [3, 12, 8, 15]
last = scores.pop()    # retire ET RENVOIE le dernier élément
print(last, scores)    # 15 [3, 12, 8]
first = scores.pop(0)  # pop(index) : retire à un index précis
print(first, scores)   # 3 [12, 8]

remove travaille par valeur, pop par index — et pop te rend l'élément, ce qui permet de l'utiliser.

Exemple 5 — trier : sort (modifie) contre sorted (renvoie)

nums = [3, 1, 2]
print(sorted(nums))    # [1, 2, 3] : une NOUVELLE liste triée...
print(nums)            # [3, 1, 2] : ...l'originale n'a pas bougé

nums.sort()            # trie SUR PLACE, ne renvoie rien d'utile
print(nums)            # [1, 2, 3]

nums.sort(reverse=True)
print(nums)            # [3, 2, 1] : ordre décroissant

words = ["poire", "Abricot", "banane"]
words.sort()
print(words)           # ['Abricot', 'banane', 'poire'] — les MAJUSCULES
                       # passent avant les minuscules (ordre du code des
                       # caractères, pas ordre du dictionnaire)

Exemple 6 — chercher : in, count, index ; et parcourir

scores = [12, 8, 19, 8]
print(8 in scores)         # True  : la valeur est quelque part dans la liste
print(99 in scores)        # False
print(scores.count(8))     # 2     : nombre d'occurrences
print(scores.index(19))    # 2     : index de la PREMIÈRE occurrence

for score in scores:       # le for du niveau 02 parcourt les listes
    print(score)           # 12 puis 8 puis 19 puis 8
print(sum(scores))         # 47 : sum() additionne toute la liste
print(min(scores), max(scores))   # 8 19

⏩ Le parcours de listes (avec enumerate, la construction en boucle, les pièges) est le cœur de la leçon 03-2.

6. Erreurs fréquentes

1) L'off-by-one de l'index : demander lst[len(lst)]

scores = [12, 8, 15]
print(scores[3])    # IndexError: list index out of range

3 éléments → index 0, 1, 2. len(scores) vaut 3, qui n'existe pas. Le dernier est scores[len(scores) - 1]… c'est-à-dire scores[-1], écris ça. C'est le cousin direct de l'off-by-one de range (⏪ niveau 02).

2) lst = lst.sort() — la liste devient None

nums = [3, 1, 2]
nums = nums.sort()   # sort() trie SUR PLACE et renvoie None
print(nums)          # None  — la liste triée est perdue !

Sur le moment, aucune erreur. Le crash arrive plus loin, au premier nums[0] (section 7). Correction : soit nums.sort() tout seul (sans =), soit nums = sorted(nums). Jamais le mélange des deux.

3) remove d'une valeur absente

fruits = ["pomme", "poire"]
fruits.remove("kiwi")   # ValueError: list.remove(x): x not in list

remove exige que la valeur existe. Si tu n'en es pas sûr, protège avec le test de la leçon : if "kiwi" in fruits:.

4) append d'une liste entière (au lieu d'extend)

a = [1, 2]
a.append([3, 4])
print(a, len(a))     # [1, 2, [3, 4]] 3   ← une liste DANS la liste !

b = [1, 2]
b.extend([3, 4])     # extend déverse les éléments un par un
print(b, len(b))     # [1, 2, 3, 4] 4

Aucune erreur, mais a contient 3 éléments dont un est une sous-liste — et tout le code suivant se comporte bizarrement. append ajoute un élément, tel quel.

5) Croire que remove retire toutes les occurrences

letters = ["a", "b", "a"]
letters.remove("a")
print(letters)       # ['b', 'a']  — seule la PREMIÈRE occurrence a disparu

Bug silencieux : le programme continue avec un "a" fantôme. Pour tout retirer, il faut une boucle — et boucler en retirant est piégé (⏩ 03-2).

6) Trier une liste aux types mélangés

mixed = [3, "un", 2]
mixed.sort()         # TypeError: '<' not supported between instances
                     # of 'str' and 'int'

Python refuse de décider si "un" est plus petit que 3. Le vrai bug est en amont : cette liste n'aurait jamais dû mélanger textes et nombres (souvent un input() non converti — ⏪ niveau 01).

7. Lire les messages d'erreur

L'exception emblématique de la leçon est IndexError :

Traceback (most recent call last):
  File "C:\Users\jtron\Claude\learnpython\best_score.py", line 2, in <module>
    print(scores[3])
          ~~~~~~^^^
IndexError: list index out of range

Lecture — toujours de bas en haut :

  1. Dernière ligne : IndexError: list index out of range = « tu demandes un casier qui n'existe pas ». C'est TOUJOURS ça : index trop grand (ou liste vide).
  2. Au-dessus : le fichier, la ligne (line 2), le code fautif — les ^^^ pointent l'accès [3], les ~~~ soulignent la liste concernée.
  3. Réflexe de diagnostic : ajoute print(len(scores), scores) juste avant la ligne fautive. Tu verras combien de casiers existent vraiment, et pourquoi ton index dépasse.

Deuxième traceback typique — la conséquence différée du bug lst = lst.sort() :

Traceback (most recent call last):
  File "C:\Users\jtron\Claude\learnpython\best_score.py", line 3, in <module>
    print(nums[0])
          ~~~~^^^
TypeError: 'NoneType' object is not subscriptable

'NoneType' object is not subscriptable = « tu mets des crochets sur None ». La ligne montrée (nums[0]) n'est pas le bug : elle est la victime. Le bug est plus haut, là où nums est devenu None — cherche un nums = nums.sort() (ou toute méthode « sur place » dont on a gardé le résultat). Quand le type d'une variable te surprend, remonte à sa dernière assignation.

Enfin, ValueError: list.remove(x): x not in list : la valeur demandée n'existe pas dans la liste — affiche la liste juste avant le remove pour voir ce qu'elle contient réellement (faute de frappe ? déjà retirée ?).

8. Exercices — faciles

Règles : un fichier par exercice dans lessons/level-03-structures-donnees/exercices/mes-reponses/, nommé ex-03-1-a.py, etc. Tape tout à la main. Prédis la sortie AVANT d'exécuter.

a) Crée une liste grades avec cinq notes de ton choix (des int). Affiche : la première note, la dernière (avec un index négatif), le nombre de notes, la moyenne (calculée avec sum et len).

b) Prédis la sortie exacte de ce programme sur papier, puis vérifie :

nums = [10, 20, 30]
nums.append(40)
nums.insert(1, 15)
nums.pop()
print(nums, len(nums))

c) Pars de shopping = ["pain", "lait", "oeufs"]. En quatre opérations et un seul print final : ajoute "cafe" au bout, remplace "lait" par "yaourt" (par index), retire "pain" (par valeur), affiche la liste et sa longueur. Sortie attendue : ['yaourt', 'oeufs', 'cafe'] 3.

9. Exercices — moyens

a) Ce programme est censé afficher Meilleur score : 19 mais plante. Trouve les DEUX bugs sans l'exécuter (il y en a un visible et un sournois), puis corrige :

scores = [12, 7, 19, 3]
scores = scores.sort()
print(f"Meilleur score : {scores[len(scores)]}")

b) Avec un for et range (⏪ niveau 02), construis la liste de tous les nombres pairs de 1 à 20 (via append), puis affiche : la liste, sa somme, sa moyenne, et son maximum. Interdiction de taper la liste à la main.

c) Écris un petit contrôleur de saisie : demande un fruit avec input(). Si le fruit est dans stock = ["pomme", "poire", "banane"], retire-le et affiche le stock restant ; sinon affiche Pas en stock : <fruit> sans planter. Teste les deux chemins.

10. Exercices — difficiles

a) Sans utiliser sort() ni sorted() : trouve le deuxième plus grand élément de nums = [12, 7, 19, 3, 15] en un seul parcours. Le programme doit afficher 19 15 (le plus grand, puis le deuxième). Teste ensuite avec d'autres listes d'au moins deux éléments.

11. Mini-projet lié — « liste de courses interactive »

Écris shopping_list.py : un menu en boucle (⏪ while du niveau 02) :

1. Ajouter  2. Retirer  3. Afficher trie  q. Quitter
> 1
Article a ajouter : cafe
cafe ajoute (1 article(s)).

Contraintes : la liste démarre vide ; « Retirer » vérifie avec in avant remove (message clair si absent) ; « Afficher » montre les articles triés alphabétiquement, un par ligne, préfixés de -, sans détruire l'ordre d'ajout de la liste (indice : sort ou sorted ?) ; liste vide → message Liste vide. ; choix inconnu → message, on redemande. Durée cible : 30–40 min. C'est le premier mini-projet du niveau annoncé dans le README — tu viens d'écrire ta première vraie application de gestion de données.

12. Correction / méthode de correction

Les solutions détaillées sont dans solutions/serie-03-1-solutions.md.

Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement, pas seulement le code ; (3) pour chaque écart, une ligne de journal : « je pensais X, en fait Y » ; (4) refais de tête l'exercice raté le lendemain. Pour les exercices de prédiction : si ta prédiction était fausse, ne te contente pas de lire la bonne réponse — déroule le programme ligne par ligne en dessinant la liste après chaque opération.

13. À retenir

14. Questions de révision

  1. Pourquoi scores[len(scores)] plante-t-il toujours, quel que soit le contenu de la liste ?
  2. Quelle est la différence entre scores.sort() et sorted(scores) ? Que vaut scores après scores = scores.sort() ?
  3. remove et pop retirent tous les deux un élément : qu'est-ce qui les distingue (argument attendu, valeur renvoyée) ?
  4. Que fait append([3, 4]) de différent d'extend([3, 4]) ?
  5. Comment retirer une valeur d'une liste sans risquer de ValueError ?
  6. Que contient letters après letters = ["a", "b", "a"] puis letters.remove("a") ?
  7. Pourquoi un TypeError: 'NoneType' object is not subscriptable sur nums[0] indique-t-il presque toujours un bug situé PLUS HAUT dans le fichier ?

15. Checklist de compréhension

Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.

16. Commit conseillé

git add lessons/level-03-structures-donnees/exercices/mes-reponses/
git commit -m "exercises: complete lesson 03-1 lists"

Puis, si c'est ta fin de session : entrée de journal + git push.