Cuando escribes a == b, Python no está preguntando “¿son el mismo objeto?”. Está preguntando “¿tienen el mismo valor?”. La diferencia parece sutil hasta que produce un bug que tarda horas en encontrar.
Hay dos conceptos completamente distintos aquí: igualdad de valor y identidad de objeto. Python los expone como operadores separados precisamente porque son cosas separadas.
== funciona llamando al método __eq__ del objeto de la izquierda. Cada clase puede definir qué significa “ser igual” para sus instancias. Una lista [1, 2, 3] es igual a otra lista [1, 2, 3] aunque sean dos objetos completamente distintos en memoria, porque list.__eq__ compara elemento a elemento.
is, en cambio, no pregunta a ningún método. Compara directamente los identificadores de objeto: lo que devuelve id(). Internamente verifica que ambas variables apunten exactamente a la misma dirección de memoria. No hay forma de sobreescribir este comportamiento desde Python.
El momento en que esto importa de verdad es cuando tienes dos objetos con el mismo valor pero distinta identidad — cosa que pasa constantemente en código real. Si usas is donde deberías usar ==, el código compila sin quejas y puede fallar de maneras intermitentes que dependen de detalles de implementación de CPython.
Hay una regla práctica que cubre el 99% de los casos: usa is únicamente para comparar con None, True y False. Para todo lo demás, ==.
# Comparando == vs is en la práctica
# --- Listas: mismo valor, distintas identidades ---
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — __eq__ compara elemento a elemento
print(a is b) # False — son dos objetos distintos en memoria
# --- None: el único caso donde is es la elección correcta ---
def process(value=None):
if value is None: # correcto: None es un singleton
return "sin valor"
return value * 2
# --- Enteros pequeños: el pool de CPython en acción ---
x = 256
y = 256
print(x is y) # True en CPython — enteros [-5, 256] están en caché
z = 257
w = 257
print(z is w) # False — fuera del rango del pool, son objetos nuevos
# --- Strings: intern pool, comportamiento no confiable ---
s1 = "hola"
s2 = "hola"
print(s1 is s2) # True en CPython (strings simples se internalizan)
s3 = "hola mundo"
s4 = "hola mundo"
print(s3 is s4) # Puede ser True o False — no lo asumas nunca
# --- El bug silencioso clásico ---
def check_flag(flag):
# ❌ Peligroso: flag podría ser cualquier objeto truthy
if flag is True:
return "activado"
return "no activado"
print(check_flag(True)) # "activado"
print(check_flag(1)) # "no activado" — bug: 1 == True pero 1 is not True
def check_flag_correcto(flag):
if flag == True: # ✅ o simplemente: if flag:
return "activado"
return "no activado"
Qué está pasando en cada caso
Las listas a y b son el ejemplo más limpio. Las creaste con la misma sintaxis y el mismo contenido, pero Python ejecutó el literal [1, 2, 3] dos veces, construyendo dos objetos independientes. == los considera iguales porque list.__eq__ hace su trabajo. is los distingue porque viven en posiciones distintas de memoria.
None es diferente: es un singleton, existe exactamente una instancia en todo el intérprete. Por eso is None siempre funciona y es además la forma idiomática recomendada por PEP 8. Usar == None funciona también, pero abre la puerta a que alguna clase con un __eq__ raro diga que es igual a None.
El comportamiento con enteros pequeños es un detalle de implementación de CPython (la implementación oficial de Python). CPython mantiene en caché los enteros entre -5 y 256 para ahorrar memoria y acelerar operaciones frecuentes. Cuando escribes x = 256, Python reutiliza el objeto que ya existe en ese pool. En cuanto pasas a 257, cada asignación crea un objeto nuevo. Este comportamiento no está garantizado por la especificación del lenguaje — PyPy o Jython pueden comportarse diferente.
Los strings tienen un mecanismo similar llamado string interning. CPython internaliza automáticamente strings que parecen identificadores válidos (sin espacios, caracteres especiales, etc.). Por eso "hola" is "hola" puede dar True, pero “puede” es la palabra clave. No es un comportamiento garantizado y cambia según cómo se construya el string (si viene de input del usuario, de una operación de concatenación, de un archivo, el resultado puede ser distinto).
El bug de flag is True merece atención especial porque es insidioso. True también es un singleton en Python, pero 1 == True es True mientras que 1 is True es False. Si tu función recibe un entero 1 como flag (cosa común cuando integras con APIs que devuelven 0/1), la versión con is la rechazará silenciosamente. La mayoría de las veces lo que realmente quieres escribir es simplemente if flag:.
Errores que debes conocer
Error: usar is para comparar strings que vienen de fuentes externas, confiando en el intern pool de CPython.
# ❌ Wrong
user_input = input("Escribe 'admin': ")
if user_input is "admin": # además, Python moderno lanza SyntaxWarning aquí
print("acceso concedido")
# ✅ Right
user_input = input("Escribe 'admin': ")
if user_input == "admin":
print("acceso concedido")
is con strings que no están internalizados siempre devolverá False, negando el acceso aunque el valor sea correcto.
Error: comprobar si una variable “existe o tiene valor” con == None en vez de is None.
# ❌ Wrong
class Raro:
def __eq__(self, other):
return True # igual a todo, incluido None
obj = Raro()
if obj == None: # True — pero obj claramente no es None
print("esto no debería ejecutarse")
# ✅ Right
if obj is None: # False — correcto, obj no es None
print("esto no debería ejecutarse")
is None es inmune a __eq__ personalizado porque no lo llama en absoluto.
N° 27