Définition
Deux noms peuvent désigner exactement la même chose, ou deux choses qui se ressemblent à s'y méprendre. À la lecture du code, la nuance ne saute pas aux yeux : si une liste est égale à une autre, s'agit-il de la même liste ou d'une copie parfaite ? Python a besoin de répondre à cette question précise, distincte de la comparaison de valeur, et c'est le rôle de is.
is compare l'identité de deux objets : il répond vrai lorsque les deux noms désignent le même objet en mémoire. C'est une question différente de celle que pose ==, qui compare les valeurs. Deux listes peuvent contenir exactement les mêmes éléments sans être le même objet, et c'est justement ce que montre l'exemple suivant.
a = [1, 2, 3]
b = [1, 2, 3]
c = a
a == b # True : mêmes valeurs
a is b # False : deux objets distincts
a is c # True : le même objet, sous deux nomsLa conséquence pratique est directe : modifier c modifie aussi a, puisqu'il s'agit du même objet sous un autre nom, alors que modifier b ne change rien à a. Retenir cette distinction évite une bonne partie des bugs silencieux, ceux où une donnée change alors que rien, en apparence, n'y a touché.
Le seul usage vraiment recommandé
Une fois cette différence comprise, une question suit aussitôt : dans du code réel, quand s'en sert-on vraiment ? La réponse tient à un seul cas courant, tester si une valeur vaut None. Ce n'est pas une question de goût : il n'existe qu'un seul objet None dans tout un programme Python, si bien que comparer par identité donne toujours le bon résultat, là où l'égalité pourrait en théorie être trompée par une classe qui redéfinit sa propre méthode de comparaison.
if resultat is None:
...
if resultat is not None:
...La même logique s'applique à True et à False, mais l'écrire ainsi est déconseillé : une condition teste déjà directement la valeur de vérité d'une expression. Écrire if actif: suffit, là où if actif is True: ajoute une étape qui n'apporte rien.
Le piège des petits nombres
C'est ici que is commence à donner des résultats déroutants, au point que beaucoup de personnes qui découvrent le langage se demandent si elles ont mal compris la section précédente. Le problème se pose ainsi : deux entiers construits séparément, avec exactement la même valeur, répondent parfois vrai à un test d'identité, et parfois faux, sans qu'aucune règle visible dans le code l'explique.
a = 256
b = 256
a is b # True, l'entier est mis en cache
a = 257
b = 257
a is b # False, deux objets distinctsL'explication tient à une optimisation interne de l'interpréteur : Python garde en mémoire une seule copie des petits entiers les plus fréquents, environ entre -5 et 256, ainsi que de certaines chaînes courtes écrites directement dans le code. Deux noms qui reçoivent la valeur 256 pointent donc sur le même objet, alors que 257 dépasse la plage mise en cache et provoque la création de deux objets distincts.
Ce comportement n'est garanti par aucune spécification du langage et peut changer d'une version de Python à l'autre. S'appuyer dessus pour comparer des valeurs revient à écrire du code qui fonctionne par accident.
La leçon dépasse ce cas précis : comparer des valeurs avec is donne un résultat qui semble juste, jusqu'au jour où il ne l'est plus. Le bug qui en résulte est pénible à trouver, puisque le code fonctionne des semaines sur de petites valeurs de test avant d'échouer sur les vraies données en production.
Ce qu'il révèle sur les copies
Il existe un bug classique, celui où une valeur change alors que rien, dans le code qu'on vient de lire, ne semble l'avoir modifiée. La cause tient souvent en une ligne écrite plus haut : affecter une liste à un nouveau nom ne la copie pas, cela ajoute seulement une étiquette de plus sur le même objet.
original = ["a", "b"]
alias = original # même objet
copie = original.copy() # objet distinct
alias.append("c")
original # ['a', 'b', 'c'] : modifié
copie # ['a', 'b'] : intactUn test avec is répond immédiatement à la question qu'on se pose alors : ai-je vraiment copié cette donnée, ou seulement créé un deuxième nom pour la même chose ? C'est souvent le premier réflexe de diagnostic quand une valeur change de façon inattendue.
La méthode .copy() ne règle qu'une partie du problème : elle crée un nouvel objet de premier niveau, mais les éléments qu'il contient restent partagés avec l'original s'ils sont eux-mêmes modifiables, comme des listes imbriquées.
Questions fréquentes
Quand faut-il utiliser is plutôt que == ?
Pour None, et pour vérifier que deux noms pointent volontairement sur le même objet. Partout ailleurs, y compris pour les nombres, les chaînes et les listes, c'est == qui donne la bonne réponse, parce que c'est la valeur qui compte, pas l'emplacement en mémoire.
Pourquoi mes deux chaînes identiques donnent-elles False avec is ?
Parce qu'elles ont été construites séparément, par exemple par concaténation ou par lecture d'un fichier. Python ne partage que certaines chaînes littérales écrites telles quelles dans le code, jamais celles calculées pendant l'exécution du programme.
Comment écrire la négation ?
Avec is not, en deux mots, plutôt qu'avec not x is y, qui se lit mal et prête à confusion. Python traite is not comme un opérateur à part entière, exactement comme in a son pendant not in.