La méthode list.append() modifie la liste en place et retourne None. Ce comportement piège encore des développeurs expérimentés qui chaînent l’appel ou assignent le résultat à une variable. Nous allons détailler ce qui se passe réellement en mémoire lors d’un append, comment CPython gère l’allocation sous-jacente, et pourquoi le contexte free-threaded de Python 3.13+ change la donne.
Allocation interne de append : la formule d’over-allocation CPython
Quand append ajoute un élément au-delà de la capacité allouée, CPython ne se contente pas de réserver une seule place supplémentaire. Le tableau dynamique sous-jacent applique une formule d’over-allocation précise.
La formule documentée dans le code source de CPython est : new_allocated = newsize + (newsize >> 3) + (3 if newsize < 9 else 6). Le facteur de croissance tourne autour de 1,125x par dépassement de capacité, loin du doublement souvent enseigné dans les cours d'algorithmique.
Cette stratégie a une conséquence directe : append reste en O(1) amorti, mais les réallocations successives produisent des copies mémoire coûteuses sur de très grandes listes. Si vous construisez une liste de plusieurs millions d'éléments par appels successifs à append, le temps passé en copie lors des réallocations devient mesurable.

Nous recommandons de pré-allouer quand la taille finale est connue. Plutôt que de boucler sur append, une list comprehension ou [None] * n suivi d'assignations par index évite les réallocations intermédiaires.
Visualiser l'empreinte mémoire réelle
Un appel à sys.getsizeof() sur une liste vide retourne une taille de base (l'objet liste lui-même plus le pointeur vers le tableau interne). Après un premier append, la taille saute : CPython a réservé de la place pour plusieurs éléments futurs.
Chaque palier d'allocation est visible en appelant sys.getsizeof() après chaque append dans une boucle. La taille reste stable pendant plusieurs ajouts, puis bondit d'un coup. Ce schéma en escalier est la signature directe de l'over-allocation.
Append et mutabilité : le piège du retour None en Python
append() retourne systématiquement None. Écrire nouvelle_liste = ma_liste.append(42) assigne None à nouvelle_liste et modifie ma_liste en place. C'est un pattern d'erreur fréquent, y compris dans du code en production.
Le problème se complique avec les listes imbriquées. Quand on appelle append avec un objet mutable (une autre liste, un dictionnaire), Python ajoute une référence, pas une copie. Modifier l'objet original après l'append modifie aussi l'élément dans la liste.
- Appeler
lst.append(sous_liste)insère une référence verssous_liste: toute modification ultérieure desous_listese reflète danslst - Pour obtenir une copie indépendante, utiliser
lst.append(sous_liste.copy())oulst.append(sous_liste[:]) - Ce comportement s'applique à tout objet mutable passé en argument : dictionnaires, sets, instances de classes
- Avec des objets immuables (int, str, tuple), le problème ne se pose pas puisque la valeur ne peut pas changer après insertion
Append vs extend vs insert : choisir la bonne méthode
append(x) ajoute l'élément x tel quel en fin de liste. extend(iterable) décompresse l'itérable et ajoute chaque élément individuellement. Confondre les deux produit des résultats très différents.
Appeler lst.append([1, 2, 3]) ajoute une liste comme élément unique. Appeler lst.extend([1, 2, 3]) ajoute trois éléments séparés. Append traite son argument comme un seul item, quel que soit son type.
insert(index, x) permet de placer un élément à une position arbitraire, mais avec un coût en O(n) puisque tous les éléments suivants doivent être décalés. Quand l'ajout en fin suffit, append est le choix performant.

Python liste append en contexte free-threaded (Python 3.13+)
L'arrivée du mode free-threaded dans Python 3.13+ modifie les garanties de sécurité thread autour des opérations sur les listes. La documentation officielle de ce mode précise que lst.append(x) et lst.pop() sont considérées comme sûres depuis plusieurs threads dans le mode free-threaded spécifiquement.
En revanche, désactiver le GIL avec python -X gil=0 ne rend pas automatiquement append thread-safe du point de vue du code Python. Des appels concurrents sur la même liste sans mécanisme de synchronisation peuvent corrompre la structure interne.
Pour du code multi-thread qui manipule une liste partagée, nous recommandons d'encapsuler les accès dans un threading.Lock ou de passer à une collections.deque dont les opérations append et popleft sont atomiques côté C.
Schéma mental pour append : trois états de la liste
Visualiser append comme une séquence de trois états aide à comprendre chaque appel sans ambiguïté.
- État 1 (avant) : la liste contient n éléments, la capacité allouée est c (avec c >= n)
- État 2 (pendant) : si n + 1 > c, CPython réalloue un nouveau tableau avec la formule d'over-allocation, copie les n éléments existants, puis place le nouvel élément en position n
- État 3 (après) : la liste contient n + 1 éléments, la capacité est soit c (pas de réallocation) soit c' calculée par la formule, et la méthode retourne None
Ce modèle en trois temps explique pourquoi append est O(1) amorti : la majorité des appels tombent dans le cas où n + 1 <= c et se résument à une écriture de pointeur plus une incrémentation du compteur de taille.

L'erreur la plus coûteuse en production n'est pas un mauvais usage syntaxique d'append. C'est d'ignorer le mécanisme de références partagées et de découvrir, après des heures de débogage, que modifier un objet mutable affecte toutes les listes qui le référencent. Garder en tête le schéma allocation/référence/retour None couvre la quasi-totalité des bugs liés à cette méthode.

