Blog

is vs == en Python: identidad e igualdad

== pregunta si dos objetos tienen el mismo valor. is pregunta si son el mismo objeto en memoria.

Coinciden lo bastante seguido como para que usar el equivocado casi siempre funcione, que es justo lo que lo vuelve un error difícil cuando no funciona.

La diferencia

Dos listas con el mismo contenido son iguales y no son idénticas.

# == compares values, is compares identity
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b, a is b)

c = a
print(a == c, a is c)

Imprime:

True False
True True

Asigna una a la otra y las dos son verdaderas, porque hay una sola lista.

Usa is para None

None es un singleton. Existe exactamente uno, así que la identidad es la pregunta correcta.

# for None, always use is
value = None
print(value is None, value == None)

Imprime:

True True

== None normalmente funciona y sigue estando mal, por la razón de la sección siguiente.

La igualdad se puede redefinir

Una clase puede hacer que == signifique lo que sea. is no se puede sobrescribir.

# == can be redefined, is cannot
class Always:
    def __eq__(self, other):
        return True

x = Always()
print(x == 'anything', x == 42)
print(x is Always())

Imprime:

True True
False

Si este objeto se comparara con == None, la comprobación pasaría y tu manejo de None correría sobre un objeto real.

Por qué los números chicos parecen idénticos

CPython cachea los enteros chicos, y las constantes dentro de un mismo objeto de código se comparten. Eso hace que is devuelva True donde esperarías False.

# small integers are cached by CPython
m, n = 256, 256
print('256:', m is n)
p = 1000
q = 1000
print('1000 literals:', p is q)          # same code object, so the constant is shared
r = int('1000')
print('1000 at runtime:', p is r, p == r)

Imprime:

256: True
1000 literals: True
1000 at runtime: False True

La última línea es la honesta. Dos 1000 construidos de la misma forma comparten un objeto aquí, pero un 1000 construido en tiempo de ejecución no. Nada de esto lo garantiza el lenguaje, así que nunca escribas comparaciones que dependan de ello.

Las cadenas hacen lo mismo

Los literales de texto se internan. Las cadenas construidas en tiempo de ejecución no.

# the same happens with short strings
s1 = 'hello'
s2 = 'hello'
print('literal:', s1 is s2)
s3 = ''.join(['hel', 'lo'])
print('built at runtime:', s1 is s3, s1 == s3)

Imprime:

literal: True
built at runtime: False True

Por esto if name is "admin" pasa en las pruebas y falla en producción, donde el nombre llega desde una petición.

La identidad es lo que te dice si algo fue mutado

Este es el lado útil de is. Muestra si una operación creó un objeto nuevo o cambió el que ya tenías.

# identity is what actually changed
first = [1, 2]
second = first
first = first + [3]         # rebinds, new object
print(first, second, first is second)

third = [1, 2]
fourth = third
third += [3]                # mutates in place
print(third, fourth, third is fourth)

Imprime:

[1, 2, 3] [1, 2] False
[1, 2, 3] [1, 2, 3] True

+ construyó una lista nueva y reasignó el nombre, así que el segundo nombre sigue viendo el valor viejo. += cambió la lista en el lugar, así que los dos nombres la ven. El mismo resultado en pantalla, comportamiento distinto para cualquiera que tenga una referencia.

Qué recordar

  • == compara valores, is compara identidad.

  • Usa is solo para None, True, False y centinelas.

  • El cacheo de enteros chicos y de literales de texto es un detalle de CPython. No construyas lógica sobre eso.

  • is es la herramienta para comprobar si algo se copió o se mutó.

Los linters marcan un is contra un literal justamente por esto. Si el tuyo no lo hace, activa esa comprobación.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.