Comparar dos valores es trivial. Lo interesante empieza cuando Python te permite escribir 0 < x < 10 exactamente igual que lo escribirías en matemáticas, y eso no es azúcar sintáctico superficial: hay una decisión de diseño concreta detrás que vale la pena entender.
Qué son y cómo funcionan internamente
Los operadores de comparación (==, !=, <, >, <=, >=) siempre devuelven un bool, ya sea True o False. Eso parece obvio, pero tiene una implicación importante: son distintos del operador de asignación = y del operador de identidad is. == pregunta ¿tienen el mismo valor?; is pregunta ¿son el mismo objeto en memoria?. Confundirlos es uno de los errores más comunes al empezar.
Lo que hace a Python especial es el encadenamiento de comparaciones. Cuando escribes:
0 < x < 10
Python no lo evalúa de izquierda a derecha como una expresión booleana ordinaria. Lo expande internamente a:
0 < x and x < 10
Pero con una diferencia crítica: x se evalúa una sola vez. Si x fuera una llamada a función con efectos secundarios, esa función se ejecutaría una vez, no dos. Esto importa en cuanto tu código crece y las expresiones dejan de ser simples variables.
¿Cuándo usar encadenamiento? Siempre que compruebes si un valor cae dentro de un rango. Es más legible, es más cercano al lenguaje matemático, y elimina la posibilidad de escribir x mal en uno de los dos lados cuando lo haces manualmente con and.
¿Qué rompe si te equivocas? En Python 3, comparar tipos incompatibles —como "hola" < 5— lanza un TypeError en lugar de producir un resultado silencioso. Python 2 inventaba un orden arbitrario entre tipos distintos, lo que generaba bugs difíciles de detectar. Python 3 decidió que es mejor un error ruidoso que un resultado sin sentido.
Ejemplo completo
def classify_temperature(celsius: float) -> str:
if celsius < -10:
return "extremadamente frío"
elif -10 <= celsius < 0:
return "bajo cero"
elif 0 <= celsius < 15:
return "frío"
elif 15 <= celsius < 25:
return "templado"
elif 25 <= celsius < 35:
return "caluroso"
else:
return "extremadamente caluroso"
def is_valid_percentage(value: float) -> bool:
# El encadenamiento evalúa 'value' una sola vez
return 0.0 <= value <= 100.0
def compare_words(a: str, b: str) -> str:
if a == b:
return f"'{a}' y '{b}' son idénticas"
elif a < b:
# Las cadenas se comparan lexicográficamente (orden Unicode)
return f"'{a}' va antes que '{b}' alfabéticamente"
else:
return f"'{a}' va después que '{b}' alfabéticamente"
samples = [-15.0, -5.0, 7.3, 20.0, 30.0, 42.0]
for temp in samples:
print(f"{temp:>6}°C → {classify_temperature(temp)}")
print()
percentages = [-1.0, 0.0, 55.5, 100.0, 101.0]
for pct in percentages:
print(f"{pct:>6}% → válido: {is_valid_percentage(pct)}")
print()
print(compare_words("manzana", "naranja"))
print(compare_words("zebra", "avestruz"))
print(compare_words("python", "python"))
Desglose del código
Fíjate en classify_temperature: cada rama usa un encadenamiento del tipo -10 <= celsius < 0. Esto garantiza que los rangos son mutuamente excluyentes y cubre todo el eje numérico sin solapamientos. Si lo hubieras escrito con and explícito, el riesgo de copiar mal el nombre de la variable o poner el operador al revés sería real. Con el encadenamiento, la intención es visible de un vistazo.
En is_valid_percentage, la expresión 0.0 <= value <= 100.0 evalúa value una única vez. Parece un detalle menor aquí, pero si value viniera de next(some_generator) o de una función que consulta una base de datos, llamarla dos veces cambiaría el comportamiento del programa. El encadenamiento te protege de ese bug sin que tengas que pensar en él.
compare_words muestra que < y > funcionan sobre cadenas comparando carácter a carácter según su valor Unicode. "manzana" < "naranja" es True porque 'm' (U+006D) viene antes que 'n' (U+006E). No es alfabético en el sentido lingüístico completo —la capitalización y los acentos pueden sorprenderte— pero para cadenas en minúsculas sin caracteres especiales el comportamiento es exactamente el que esperas.
El operador != es la negación directa de ==: a != b equivale a not (a == b). No existe !> ni !<; para eso usas <= y >= respectivamente.
Errores que debes conocer
Error: usar == para comparar con None, lo que funciona pero rompe si alguien define __eq__ en una clase personalizada de manera inesperada.
# ❌ Wrong
def process(data):
if data == None:
return "sin datos"
# ✅ Right
def process(data):
if data is None:
return "sin datos"
is None comprueba identidad de objeto, no igualdad de valor. None es un singleton en Python, así que esta es siempre la forma correcta.
Error: comparar tipos incompatibles asumiendo que Python resolverá algo razonable, como hacía Python 2.
# ❌ Wrong scores = [85, "N/A", 90, "pendiente"] passing = [s for s in scores if s >= 60] # TypeError en el segundo elemento # ✅ Right scores = [85, "N/A", 90, "pendiente"] passing = [s for s in scores if isinstance(s, (int, float)) and s >= 60]
Filtrar por tipo antes de comparar evita el TypeError y hace explícita la intención: solo los valores numéricos participan en la comparación.
Error: encadenar comparaciones esperando que funcionen como en otros lenguajes, sin saber que Python las evalúa de forma especial.
# ❌ Wrong — en otros lenguajes esto sería (True > 0) que da True, no lo que parece result = 1 < 2 > 0 # Python evalúa: (1 < 2) and (2 > 0) → True # ✅ Right — si quieres lógica booleana combinada, sé explícito a, b = True, True result = a and b
En Python 1 < 2 > 0 es una comparación encadenada válida y da True, pero si lo escribiste queriendo combinar resultados booleanos, usa and de forma explícita para que la intención sea clara.
N° 25