Python

== vs is en Python: la diferencia que causa bugs misteriosos

Hay un bug que aparece una y otra vez en código Python, tan sutil que muchos no lo ven hasta que todo se quiebra:

if variable == None:
    print("Es nulo")

Esto parece correcto. Funciona. Pero un pythonista experimentado levantaría la mano y diría: “Deberías usar is, no ==.”

¿Por qué? ¿Acaso == None no funciona?

Funciona. Pero hay una diferencia entre “funciona” y “es lo correcto”. Y esa diferencia te causará dolores de cabeza si no la entiendes.

La confusión es real: == e is parecen hacer lo mismo (comparar dos cosas). Pero comparan cosas distintas. Y elegir la equivocada es la causa de algunos de los bugs más raros y difíciles de debuguear en Python.

En esta entrada vamos a ver:

  • Qué compara cada uno.
  • Por qué son distintos.
  • Dónde está la trampa (los números pequeños y None).
  • Cuál usar en cada situación.
  • Por qué en algunos casos dan resultados “raros”.

La diferencia en una frase

  • == compara valor: “¿tienen el mismo contenido?”
  • is compara identidad: “¿son el mismo objeto en memoria?”

Eso es todo. Pero las consecuencias son enormes.

El ejemplo que muestra todo

a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  # True  ← mismo contenido
print(a is b)  # False ← diferentes objetos en memoria

Ahora lo mismo pero con tuplas:

c = (1, 2, 3)
d = (1, 2, 3)

print(c == d)  # True  ← mismo contenido
print(c is d)  # True  ← ¡mismo objeto en memoria!

¿Qué? ¿Cómo es posible que dos tuplas distintas sean el mismo objeto?

Respuesta: Python cachea tuplas pequeñas por optimización. Crea una sola tupla en memoria y reutiliza la referencia.

Eso es el primer indicio de que == e is no son lo mismo, y que Python tiene trucos internos que hacen que el comportamiento sea raro a veces.

Entender la identidad: la dirección de memoria

Cada objeto en Python vive en una dirección de memoria. Dos objetos distintos tienen direcciones distintas, aunque tengan el mismo valor.

a = [1, 2, 3]
b = [1, 2, 3]

print(id(a))  # 140234567891520 (dirección de memoria)
print(id(b))  # 140234567891648 (dirección distinta)

print(a is b)  # False (diferentes direcciones)
print(a == b)  # True (mismo contenido)

id() te da la dirección de memoria del objeto. Si dos variables apuntan a la misma dirección, is devuelve True.

c = a

print(id(a))  # 140234567891520
print(id(c))  # 140234567891520 (¡misma dirección!)
print(a is c)  # True (mismo objeto)
print(a == c)  # True (y mismo contenido, obvio)

Cuando haces c = a, no copias el contenido. Haces que c apunte a la misma dirección que a. Son alias del mismo objeto.

Las 3 comparaciones que importan

Comparar con None (SIEMPRE usa is)

Este es el bug clásico y frecuente.

# Incorrecto (pero funciona a veces)
if variable == None:
    print("Es nulo")

# Correcto
if variable is None:
    print("Es nulo")

¿Por qué? Porque None es un singleton en Python. Solo existe UNA instancia de None en toda tu aplicación. Es un objeto especial:

a = None
b = None

print(id(a))  # 94653618254496
print(id(b))  # 94653618254496 (¡la misma dirección!)
print(a is b)  # True

Usar == None funciona, pero es conceptualmente incorrecto. Preguntas “¿este objeto tiene el mismo valor que None?” cuando lo que realmente quieres preguntar es “¿este objeto ES None?”

# Esto "funciona" pero es engañoso
class Raro:
    def __eq__(self, other):
        if other is None:
            return True  # Siempre dice que es igual a None
        return False

raro = Raro()
print(raro == None)  # True (aunque no sea None)
print(raro is None)  # False (que es la verdad)

Si usas ==, alguien podría sobrescribir el comportamiento y tu código fallaría silenciosamente. Con is, es imposible engañar.

Regla de oro: siempre compara None con is, nunca con ==.

if variable is None:
    pass

if variable is not None:
    pass

Comparar booleanos (cuidado)

Los booleanos True y False son también singletons:

a = True
b = True

print(a is b)  # True (mismo objeto)
print(a == b)  # True (y mismo valor, obvio)

Así que para booleanos, == e is suelen dar el mismo resultado. Pero usa == por consistencia, no is:

# Correcto
if variable == True:
    pass

if variable == False:
    pass

# Aunque funcione, es un antipatrón
if variable is True:
    pass

Por qué? Porque creas hábitos. Si siempre usas is para booleanos, uno de estos días lo harás con otro tipo de objeto donde is no es lo correcto.

Números pequeños (la trampa silenciosa)

Python cachea enteros pequeños (del -5 al 256) para optimización:

a = 5
b = 5

print(a is b)  # True (¡Python reutiliza el mismo objeto!)
print(a == b)  # True

# Pero números más grandes...
c = 257
d = 257

print(c is d)  # False (¡diferentes objetos!)
print(c == d)  # True (mismo valor)
a = 256
b = 256
print(a is b)  # True (cacheado)

c = 257
d = 257
print(c is d)  # False (no cacheado)

Esto es una trampa invisible. Tu código funciona con números pequeños porque is accidentalmente devuelve True. Pero falla con números grandes.

# Código tramposo
if numero is 5:
    print("Es 5")

# Funciona para 5 (cacheado), pero falla para 257 (no cacheado)
if numero is 257:
    print("Es 257")  # Probablemente falle

Nunca compares números con is. NUNCA.

# Correcto
if numero == 5:
    pass

if numero == 257:
    pass

Tabla resumen: cuándo usar cada uno

SituaciónUsaEjemplo
Comparar con Noneisif variable is None:
Comparar con True/False==if variable == True:
Comparar números==if numero == 5:
Comparar strings==if nombre == "Juan":
Comparar listas/dicts==if lista == [1, 2, 3]:
Verificar si es el MISMO objetoisif lista is variable:
Comparar con negación de Noneis notif variable is not None:

Casos donde is es correcto (y necesario)

Verificar que dos variables apuntan al MISMO objeto

original = [1, 2, 3]
copia = original  # No es copia, es alias

print(original is copia)  # True (mismo objeto)
print(original == copia)  # True (mismo contenido)

# Ahora modifica uno
original.append(4)
print(copia)  # [1, 2, 3, 4] ← cambió sin querer!

Usar is te ayuda a detectar cuándo dos variables son alias peligrosos.

Detectar cambios inesperados (por referencia)

def procesar_lista(lista):
    # Alguien pasó la lista correcta?
    if lista is None:
        print("Error: lista es None")
        return

    # Modificar la lista afecta al original
    lista.append(999)

datos = [1, 2, 3]
procesar_lista(datos)
print(datos)  # [1, 2, 3, 999] ← cambió

Singleton patterns

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:  # Usa is para verificar singleton
            cls._instance = super().__new__(cls)
        return cls._instance

c1 = Config()
c2 = Config()

print(c1 is c2)  # True (mismo objeto en memoria)

La trampa visual: código que parece funcionar pero no

def validar_usuario(edad):
    # Esto "funciona" para algunos números
    if edad is 18:
        print("Eres adulto")
    else:
        print("No eres adulto")

validar_usuario(18)   # Funciona (18 está cacheado)
validar_usuario(257)  # Parece funcionar pero es casualidad
validar_usuario(1000) # Probablemente falle

El bug no se ve porque parece funcionar. El problema aparece solo con números grandes, en producción, cuando nadie espera el error.

Comparaciones especiales con is

is not

if variable is not None:
    print("Es algo")

# Equivalente (pero menos legible)
if not (variable is None):
    print("Es algo")

Usa is not cuando quieras la negación clara.

Comparar con listas vacías (INCORRECTO)

# Incorrecto
if lista is []:
    print("Lista vacía")

# Correcto
if lista == []:
    print("Lista vacía")

# Aún mejor
if not lista:
    print("Lista vacía")

Cada [] es un objeto nuevo, así que is [] siempre devuelve False (comparas objetos distintos).

El debugging: cómo encontrar estos bugs

Si sospechas que alguien usó is donde debería usar ==:

# El código falla a veces pero no siempre
# Síntoma: funciona con números pequeños, falla con grandes

numero = 500
if numero is 500:  # Falla
    print("Es 500")
else:
    print("No es 500")  # Aquí entra, aunque numero == 500 es True

Busca comparaciones is que no sean con None, True, False, o variables. Probablemente sea un bug.

# Probables bugs
if edad is 18:
if contador is 0:
if resultado is "error":
if lista is []:

# Correcto
if edad == 18:
if contador == 0:
if resultado == "error":
if not lista:

Resumen: la regla práctica

Usa == por defecto. Solo usa is en estos casos:

  1. Comparar con None: if variable is None:
  2. Comparar con not None: if variable is not None:
  3. Verificar identidad: if variable is otra_variable: (cuando necesitas saber si son el MISMO objeto)
  4. Casos muy específicos como singletons.

Nunca uses is para:

  • Números
  • Strings
  • Listas, diccionarios, tuplas
  • Booleanos (usa ==)
  • Nada que no sea None

Un ejemplo de bug real

# Pipeline de procesamiento de datos
def procesar_datos(datos, estado_esperado):
    if datos is estado_esperado:  # Bug sutil
        print("Datos en el estado correcto")
        return procesar(datos)
    else:
        print("Estado incorrecto")
        return None

resultado = procesar_datos([1, 2, 3], [1, 2, 3])
# Falla: compara objetos (direcciones), no valores
# datos y estado_esperado son listas distintas, aunque iguales

Arreglo:

def procesar_datos(datos, estado_esperado):
    if datos == estado_esperado:  # Correcto
        print("Datos en el estado correcto")
        return procesar(datos)
    else:
        print("Estado incorrecto")
        return None

Conclusiones

== e is parecen lo mismo, pero comparan cosas muy distintas:

  • == → “¿tienen el mismo valor?” (lo que casi siempre quieres)
  • is → “¿son el mismo objeto en memoria?” (lo que rara vez quieres)

El 99% del tiempo, quieres ==.

Usa is solo para:

  1. None: siempre if variable is None:
  2. Identidad: cuando necesites saber si dos variables apuntan al MISMO objeto.
  3. Casos muy específicos y documentados.

La trampa es que Python cachea algunos valores (números pequeños, tuplas, None, True, False) para optimización. Así que is a veces devuelve True cuando no esperas, haciendo que parezca que tu código funciona cuando en realidad es incorrecto. Ese es el bug invisible.

Cuando estés debugueando algo raro y no encuentres la causa, busca is. Apuesto a que está ahí.

Imagen de Mediamodifier en Pixabay

¿Te ha parecido de utilidad el contenido?

Daniel Rodríguez

Share
Published by
Daniel Rodríguez

Recent Posts

Curiosidad: El puerto 80 y los números de puerto

Durante décadas, cuando escribías una dirección web y pulsabas Intro, tu navegador hacía algo que…

5 días ago

Cómo Proyectar Cuándo Alcanzarás la Independencia Financiera

La independencia financiera es el punto en el que tus inversiones generan suficientes rendimientos para…

1 semana ago

Cómo Elegir el Test de Hipótesis Correcto para tus Datos

Tienes dos grupos de datos y quieres saber si son estadísticamente distintos. O tienes una…

2 semanas ago

Tuplas vs Listas en Python: parecen lo mismo, pero no

A primera vista, tuplas y listas son casi idénticas. Ambas guardan colecciones ordenadas de elementos.…

2 semanas ago

Qué significa realmente “calidad de datos”

"Nuestros datos no tienen buena calidad." Escuchas esto constantemente en reuniones. El problema es que…

3 semanas ago

Cómo Detectar Pseudo-IDs y Data Leakage en el Constructor de Scorecards

El artículo Data Leakage en Credit Scoring explicó por qué este es el error más…

3 semanas ago

This website uses cookies.