Python liste append et listes imbriquées : maîtriser les niveaux

La méthode append en Python ajoute un élément unique à la fin d’une liste. Quand cet élément est lui-même une liste, le résultat n’est pas un ajout à plat mais la création d’un niveau supplémentaire d’imbrication. Cette mécanique, limpide en apparence, provoque des bugs récurrents dès que le code manipule des listes imbriquées sur plusieurs niveaux.

Pourquoi append crée un niveau de liste imbriquée plutôt qu’un ajout à plat

append traite son argument comme un objet unique, quel que soit son type. Si vous passez une liste en argument, Python insère cette liste entière comme dernier élément, sans la décomposer.

A lire également : Gagner du temps avec le webmail Académie Bordeaux grâce aux filtres

Prenons un cas concret : a = [1, 2] puis a.append([3, 4]). Le résultat est [1, 2, [3, 4]], pas [1, 2, 3, 4]. La liste a contient maintenant trois éléments, dont le troisième est une sous-liste.

C’est le comportement attendu, et la documentation officielle de Python le confirme : append est équivalent à a[len(a):] = [x], où x reste un objet unique. Pour obtenir un ajout à plat, il faut utiliser extend, qui itère sur l’argument et ajoute chaque élément individuellement.

A lire également : Comment résoudre une équation complexe avec l'adresse IP 37.117.117.230

Confondre append et extend est la source principale d’imbrication involontaire. Le réflexe de vérification consiste à inspecter le type() du dernier élément après l’opération : si c’est list alors que vous attendiez int ou str, c’est qu’un append aurait dû être un extend.

Programmeuse Python sur un canapé utilisant un IDE avec des listes Python imbriquées affichées à l'écran

Référence partagée et mutation : le piège silencieux de append sur une sous-liste

Un problème plus subtil survient quand la même sous-liste est référencée à plusieurs endroits. Python ne copie pas l’objet passé à append, il stocke une référence vers celui-ci.

Exemple classique :

ligne = [0, 0, 0]
grille = []
for _ in range(3): grille.append(ligne)

La variable grille contient maintenant trois éléments, mais les trois pointent vers le même objet en mémoire. Modifier grille[0][1] = 5 change aussi grille[1][1] et grille[2][1].

Chaque append a inséré la même référence, pas une copie indépendante. La correction consiste à créer une nouvelle liste à chaque itération : grille.append(ligne[:]) (copie superficielle) ou grille.append([0, 0, 0]) directement dans la boucle.

Pour des listes imbriquées sur plus de deux niveaux, une copie superficielle ne suffit pas. Il faut alors recourir à copy.deepcopy, qui duplique récursivement chaque sous-liste. Le coût mémoire augmente, mais c’est le seul moyen d’obtenir des structures véritablement indépendantes.

Construire une liste imbriquée proprement avec append et les compréhensions

Les guides de style de la PSF et de projets comme Django ou FastAPI recommandent de privilégier les compréhensions de listes plutôt que des enchaînements de append pour construire des structures imbriquées. La raison est double : lisibilité et réduction des erreurs de niveau.

Construire une matrice 3×3 avec append :

matrice = []
for i in range(3):
  ligne = []
  for j in range(3):
    ligne.append(i * 3 + j)
  matrice.append(ligne)

La même construction en compréhension de liste :

matrice = [[i * 3 + j for j in range(3)] for i in range(3)]

Les deux produisent le même résultat. En revanche, la compréhension rend explicite la structure à deux niveaux en une seule ligne, ce qui limite le risque d’insérer un append au mauvais niveau d’indentation.

Quand append reste le bon choix

append garde l’avantage dans les cas où la logique de construction est conditionnelle ou dépend d’un état externe :

  • Ajout d’une sous-liste uniquement si une condition est remplie (validation de données, filtrage à la volée)
  • Construction incrémentale où chaque itération peut modifier la structure de la sous-liste avant de l’insérer
  • Lecture ligne par ligne d’un fichier ou d’un flux réseau, où la taille finale n’est pas connue à l’avance

Dans ces situations, forcer une compréhension de liste produit du code difficile à relire, avec des expressions ternaires imbriquées.

Aplatir une liste imbriquée en Python : itertools.chain et alternatives

L’opération inverse de l’imbrication, l’aplatissement, pose aussi des questions de niveau. itertools.chain.from_iterable aplatit un seul niveau d’imbrication, pas plus.

Pour une liste comme [[1, 2], [3, 4], [5, 6]], list(itertools.chain.from_iterable(donnees)) produit [1, 2, 3, 4, 5, 6]. Pour une liste à trois niveaux comme [[[1, 2], [3]], [[4, 5]]], il faut une fonction récursive ou un passage par NumPy avec numpy.ndarray.flatten() si les sous-listes ont des dimensions régulières.

  • itertools.chain.from_iterable : aplatit un niveau, performant, ne crée pas de liste intermédiaire
  • Fonction récursive maison : gère un nombre arbitraire de niveaux, mais attention à la profondeur de récursion sur de très grandes structures
  • numpy.array().flatten() : adapté aux données numériques homogènes, convertit la structure en tableau NumPy puis aplatit

Étudiants en informatique analysant des schémas de listes Python imbriquées sur un tableau blanc en salle de cours

Typage et listes imbriquées : détecter les erreurs de niveau avant l’exécution

Depuis Python 3.10, la PEP 604 permet d’écrire des annotations de type comme list[list[int | str]] sans importer Union. Cette simplification syntaxique a rendu le typage des listes imbriquées nettement plus accessible.

Les analyseurs statiques comme mypy et Pyright exploitent ces annotations pour signaler les confusions entre list[int] et list[list[int]] avant même l’exécution du code. Si une fonction attend un argument de type list[list[int]] et reçoit un list[int] parce qu’un append a été omis (ou ajouté en trop), l’erreur apparaît dans l’éditeur.

L’adoption croissante de ces outils dans les projets open source populaires a poussé de nombreuses équipes à annoter systématiquement leurs fonctions manipulant des structures imbriquées. Le gain n’est pas seulement théorique : sur un pipeline de données où une liste à deux niveaux se retrouve aplatie par erreur, le bug peut passer inaperçu pendant des semaines si les tests ne couvrent pas la structure.

Annoter le type attendu et lancer mypy en intégration continue reste la méthode la plus fiable pour garantir que chaque append opère au bon niveau d’imbrication.