Définition
Vous affectez une liste à une nouvelle variable, vous la modifiez par cette variable, et la liste d'origine change aussi, sans que vous l'ayez touchée directement. Cette surprise a une cause précise : certains objets se modifient sur place, d'autres non.
Un objet mutable est un objet que Python autorise à changer sans en fabriquer un nouveau. La liste en est l'exemple le plus courant : après un append, c'est le même objet qui a grossi, et non une copie qui aurait pris sa place.
couleurs = ["rouge", "vert"]
avant = id(couleurs)
couleurs.append("bleu")
print(couleurs, id(couleurs) == avant)
# ['rouge', 'vert', 'bleu'] TrueLe contraire s'appelle immuable : un tuple ou une chaîne de caractères ne se modifie jamais, toute opération qui semble le faire construit en réalité un objet neuf. La distinction décide de ce qui arrive dès que deux noms désignent le même objet.
Ce qui est mutable, et ce qui ne l'est pas
Une question suit naturellement : quels types se comportent comme la liste, et lesquels comme le tuple ? Le tableau qui suit répond type par type.
| Type | Modifiable sur place |
|---|---|
| list | Oui, par append, insert ou un tri en place |
| dict | Oui, par ajout, mise à jour ou suppression d'une clé |
| set | Oui, par add et discard |
| bytearray | Oui, octet par octet |
| Une instance de votre classe | Oui par défaut, sauf précaution explicite |
| int, float, bool, str, tuple | Non, en aucune circonstance |
| frozenset | Non, c'est la version figée de l'ensemble |
Un repère évite d'apprendre le tableau par cœur : les méthodes qui ne renvoient rien ont travaillé sur place, celles qui renvoient un résultat en ont fabriqué un nouveau. couleurs.sort() ne rend aucune valeur, puisqu'elle a trié la liste elle-même, là où sorted(couleurs) en construit une seconde qu'il faut récupérer.
Deux noms pour un même objet
Une affectation comme sauvegarde = panier ne copie rien : elle crée un second nom qui pointe vers le même objet, ce que confirme le test is. Sur un objet immuable, cela ne se remarque jamais. Sur un objet mutable, tout ce qui passe par un nom devient visible depuis l'autre.
panier = ["pain"]
sauvegarde = panier # le même objet, pas une copie
panier.append("lait")
print(sauvegarde) # ['pain', 'lait']La copie se demande explicitement : list(panier), un slicing complet écrit panier[:], ou copy.copy. Ces trois formes produisent une copie de surface : la liste extérieure est neuve, mais les objets rangés dedans restent partagés. Pour une liste de listes, seule copy.deepcopy descend jusqu'au bout.
Le schéma ci-dessous le montre : deux noms sur un même objet, un troisième sur une copie.
Essayez-le vous-même avec la démonstration ci-dessous.
Au départ, les trois noms affichent le même contenu. Rien ne permet de deviner lequel partage sa mémoire avec les autres.
Un test avec is répond immédiatement à la question : original is alias vaut True, original is copie vaut False. C'est le premier réflexe de diagnostic quand une donnée change sans que rien ne semble y toucher.
Les trois pièges qui coûtent le plus cher
Le premier est la multiplication d'une liste qui contient elle-même des listes. Elle répète la référence, pas le contenu.
grille = [[0] * 3] * 3
grille[0][0] = 1
print(grille)
# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]Les trois lignes sont la même liste regardée trois fois. La forme correcte passe par une compréhension de liste, [[0] * 3 for _ in range(3)], qui reconstruit une ligne neuve à chaque tour.
Le deuxième est la valeur par défaut mutable d'une fonction, évaluée une seule fois à la définition et non à chaque appel : la même liste sert alors tous les appels et grossit sans fin.
Écrire def ajouter(article, panier=[]): semble anodin, mais ce panier par défaut n'est créé qu'une fois et partagé par tous les appels. La convention est d'écrire panier=None, puis de créer la liste dans le corps de la fonction, None marquant l'absence.
Le troisième est l'attribut mutable déclaré au niveau de la classe. Une liste écrite là appartient à la classe et non aux objets : tous les exemplaires la partagent, et l'ajout fait pour l'un apparaît chez les autres. Sa place est dans __init__.
Pourquoi un mutable ne peut pas servir de clé
Pourquoi une liste ne peut-elle pas être une clé de dictionnaire, quand un tuple identique le peut ? Un dictionnaire range ses éléments d'après une empreinte calculée à partir de la valeur. Si elle changeait après coup, l'empreinte deviendrait fausse et l'élément introuvable. Python coupe court et refuse l'opération.
index = {}
index[["a", "b"]] = 1
# TypeError: unhashable type: 'list'La TypeError est une protection, pas un caprice. Le remède consiste à figer la clé : un tuple à la place de la liste, un frozenset à la place de l'ensemble. C'est la meilleure réponse à qui demande pourquoi le tuple existe alors que la liste semble suffire.
Questions fréquentes
Comment savoir si un objet est mutable ?
En essayant hash(objet) dans une console : ce qui refuse d'être haché est mutable, et ce qui accepte ne l'est presque jamais. Le second indice est la présence de méthodes qui ne renvoient aucune valeur, signe qu'elles ont travaillé directement sur l'objet.
Un tuple qui contient une liste est-il modifiable ?
Le tuple reste immuable : aucun de ses emplacements ne peut recevoir autre chose. La liste rangée à l'intérieur continue de vivre sa vie et peut changer. Ce tuple perd du même coup le droit de servir de clé, puisque son empreinte ne serait plus fiable.
Comment rendre mes propres objets non modifiables ?
La voie la plus simple est une dataclass déclarée avec frozen=True, qui refuse toute réaffectation d'attribut après la construction.