Los operadores == e is pueden parecer equivalentes cuando se prueban con valores pequeños, pero responden preguntas diferentes. == comprueba si dos objetos tienen valores equivalentes, mientras que is comprueba si ambas referencias apuntan exactamente al mismo objeto. Confundir igualdad con identidad produce errores sutiles, especialmente con listas, strings, copias y valores opcionales.
Para comprender el tema, revisa la guía de operadores en Python, el artículo sobre listas y copias, la explicación de tipos de datos, la guía de clases y objetos y el tutorial de manejo de errores.
Las fuentes externas principales son la referencia oficial de comparaciones y la documentación oficial del modelo de datos de Python.
Igualdad de valores con ==
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True
Las listas contienen los mismos elementos en el mismo orden, por lo que se consideran iguales. Python utiliza el método especial __eq__() del tipo para decidir el resultado.
Identidad con is
a = [1, 2, 3]
b = [1, 2, 3]
print(a is b) # False
Aunque sus valores coinciden, son dos listas creadas por separado. Cada una ocupa su propia identidad durante la ejecución.
Dos nombres para el mismo objeto
a = [1, 2, 3]
b = a
print(a == b) # True
print(a is b) # True
b.append(4)
print(a)
La asignación no crea una copia. a y b apuntan a la misma lista, por lo que una modificación realizada mediante cualquiera de los nombres se observa desde el otro.
Comprobar la identidad con id()
a = {"activo": True}
b = a
c = {"activo": True}
print(id(a) == id(b))
print(id(a) == id(c))
id() devuelve un identificador único durante la vida del objeto. Se utiliza para aprendizaje y diagnóstico, pero el código normal debe expresar identidad con is.
La regla importante para None
resultado = None
if resultado is None:
print("No existe un resultado")
None es un objeto único utilizado para representar ausencia de valor. La recomendación estándar es comparar con is None o is not None.
if resultado is not None:
procesar(resultado)
Además de ser más claro, este patrón evita depender de una implementación personalizada de __eq__().
No uses is con strings y números
nombre = "Python"
if nombre == "Python":
print("Coincide")
La comparación correcta para valores es ==. Algunas pruebas con strings cortos o enteros pequeños pueden hacer que is devuelva True debido a optimizaciones internas, pero ese comportamiento no debe utilizarse como lógica del programa.
Por qué algunas pruebas engañan
a = 10
b = 10
print(a is b)
Un intérprete puede reutilizar ciertos objetos inmutables. Esto se conoce como interning o caché interna. El detalle depende del intérprete, del contexto y de la compilación. Que dos valores compartan identidad en una prueba no convierte is en un operador de igualdad.
Copias superficiales
original = [[1, 2], [3, 4]]
copia = original.copy()
print(original == copia) # True
print(original is copia) # False
print(original[0] is copia[0]) # True
La lista exterior es nueva, pero sus elementos internos siguen compartidos. Por eso se llama copia superficial.
copia[0].append(99)
print(original)
La modificación afecta a la lista interna observada por ambos objetos.
Copias profundas
from copy import deepcopy
original = [[1, 2], [3, 4]]
copia = deepcopy(original)
print(original == copia)
print(original is copia)
print(original[0] is copia[0])
deepcopy intenta copiar también los objetos anidados. No siempre es necesario y puede ser costoso o inapropiado para recursos como archivos, conexiones y objetos con estado externo.
Objetos personalizados y __eq__
class Producto:
def __init__(self, codigo, nombre):
self.codigo = codigo
self.nombre = nombre
def __eq__(self, otro):
if not isinstance(otro, Producto):
return NotImplemented
return self.codigo == otro.codigo
p1 = Producto(10, "Teclado")
p2 = Producto(10, "Teclado mecánico")
print(p1 == p2) # True
print(p1 is p2) # False
La clase define igualdad según el código. Son instancias diferentes, pero representan el mismo producto según la regla del dominio.
Devolver NotImplemented
Cuando el otro objeto no es compatible, __eq__() debería devolver NotImplemented en lugar de lanzar una excepción o devolver siempre False. Esto permite que Python intente la comparación inversa y maneje correctamente tipos diferentes.
Igualdad en dataclasses
from dataclasses import dataclass
@dataclass
class Punto:
x: int
y: int
p1 = Punto(2, 3)
p2 = Punto(2, 3)
print(p1 == p2)
print(p1 is p2)
Las dataclasses generan un método de igualdad basado en campos, salvo que se configure lo contrario.
Comparaciones con booleanos
activo = True
if activo:
print("Está activo")
No necesitas escribir activo == True ni activo is True en la mayoría de condiciones. Utiliza directamente el valor booleano. Sin embargo, si una variable puede ser True, False o None, una comprobación de identidad puede expresar una diferencia intencional:
if estado is None:
print("Sin configurar")
elif estado:
print("Activado")
else:
print("Desactivado")
Sentinelas personalizados
A veces None es un valor válido y necesitas distinguir “no proporcionado”:
NO_PROPORCIONADO = object()
def configurar(valor=NO_PROPORCIONADO):
if valor is NO_PROPORCIONADO:
return "Usar configuración predeterminada"
if valor is None:
return "Desactivar configuración"
return f"Configurar: {valor}"
Un objeto creado como sentinela se compara por identidad. Nadie puede producirlo accidentalmente desde una entrada común.
Igualdad en colecciones
lista = [1, 2, 3]
tupla = (1, 2, 3)
print(lista == [1, 2, 3])
print(lista == tupla)
Cada tipo define sus reglas. Listas y tuplas con los mismos elementos no se consideran iguales porque son estructuras distintas. Los diccionarios comparan pares clave-valor sin depender del orden de inserción.
NaN y comparaciones inesperadas
nan = float("nan")
print(nan == nan) # False
print(nan is nan) # True
NaN sigue reglas especiales de punto flotante y no es igual a sí mismo. Para detectarlo utiliza math.isnan(), no identidad ni igualdad.
Pruebas correctas
def test_listas_equivalentes():
esperado = [1, 2, 3]
obtenido = construir_lista()
assert obtenido == esperado
Las pruebas normalmente verifican resultados por valor. Utiliza identidad cuando el contrato exige devolver el mismo objeto, por ejemplo en una caché o un singleton.
Tabla mental sencilla
Pregunta “¿tienen el mismo valor?” y utiliza ==. Pregunta “¿son exactamente el mismo objeto?” y utiliza is. Para None y sentinelas, usa identidad. Para texto, números, listas, diccionarios y resultados calculados, usa igualdad.
Errores frecuentes
Los errores más comunes son escribir texto is "hola", comparar números con identidad, asumir que una asignación crea una copia y utilizar una copia superficial para estructuras anidadas. También es frecuente implementar __eq__() sin comprobar el tipo o sin devolver NotImplemented.
Conclusión
== compara valores según las reglas del tipo; is compara identidad. Dos objetos pueden ser iguales sin ser el mismo objeto, y dos nombres pueden compartir identidad aunque parezcan variables independientes. Utiliza is None para ausencia, is para sentinelas y == para datos. Esta diferencia evita errores de memoria compartida, comparaciones frágiles y comportamientos que cambian entre ejecuciones.






