Ejecuta esto en tu intérprete de Python:
>>> 0.1 + 0.2 == 0.3 False
Si eso te parece un bug, no estás solo — pero no es un bug de Python. Es una consecuencia directa de cómo los ordenadores representan números decimales en memoria, y afecta a casi todos los lenguajes modernos: JavaScript, Java, C, Rust… todos.
¿Qué es IEEE 754 y por qué importa?
Un float en Python no es un número decimal arbitrario. Es un número de punto flotante de doble precisión según el estándar IEEE 754, el mismo formato que usa tu CPU directamente en hardware. Ocupa exactamente 64 bits: 1 bit de signo, 11 bits de exponente y 52 bits de mantisa (la parte significativa del número).
El problema fundamental es que ese formato trabaja en base 2, no en base 10. Igual que 1/3 no tiene representación exacta en decimal (0.333333…), 0.1 no tiene representación exacta en binario. La fracción 1/10 en binario es una serie infinita que se trunca en los 52 bits disponibles. Lo que Python almacena como 0.1 es en realidad algo así como 0.1000000000000000055511151231257827021181583404541015625.
Cuando sumas 0.1 + 0.2, ambos valores ya tienen ese pequeño error de representación, y el resultado acumulado no coincide con el error que tiene 0.3 almacenado por separado. No es imprecisión de Python — es aritmética binaria exacta sobre representaciones inevitablemente imprecisas.
Esto importa en producción: si usas == para comparar floats, vas a tener bugs sutiles que solo aparecen en ciertos valores o tras cierto número de operaciones. Y si acumulas operaciones financieras con float, los errores de redondeo se suman.
Valores especiales: inf y nan
IEEE 754 también define valores especiales que Python expone directamente. float('inf') representa infinito positivo — útil como valor inicial en algoritmos de minimización. float('nan') (Not a Number) representa resultados indefinidos como 0/0 o sqrt(-1). Tienen propiedades aritméticas que pueden sorprenderte si no las conoces.
import math
# Infinito
pos_inf = float('inf')
neg_inf = float('-inf')
print(pos_inf + 1) # inf — infinito más cualquier número finito sigue siendo infinito
print(pos_inf + neg_inf) # nan — infinito menos infinito es indefinido
print(1 / pos_inf) # 0.0 — límite correcto
# NaN tiene una propiedad única: no es igual a nada, ni a sí mismo
nan = float('nan')
print(nan == nan) # False — el único valor en Python que no es igual a sí mismo
print(nan != nan) # True
print(math.isnan(nan)) # True — la forma correcta de detectar NaN
# Comprobaciones útiles del módulo math
print(math.isinf(pos_inf)) # True
print(math.isfinite(pos_inf)) # False
print(math.isfinite(3.14)) # True
El código que deberías escribir
import math
from decimal import Decimal, ROUND_HALF_UP
# ─── Comparación de floats ────────────────────────────────────────────────────
a = 0.1 + 0.2
b = 0.3
# ❌ Nunca hagas esto con floats
# if a == b: ...
# ✅ math.isclose() compara con tolerancia relativa y absoluta
print(math.isclose(a, b)) # True
print(math.isclose(a, b, rel_tol=1e-9)) # True — tolerancia relativa de 1 en 10^9
print(math.isclose(0.0, 1e-15, abs_tol=1e-9)) # True — abs_tol evita problemas cerca del cero
# Para entender qué hay realmente en memoria:
print(repr(0.1)) # 0.1 — Python lo muestra limpio
print(f"{0.1:.60f}") # 0.100000000000000005551115123125782702... — la verdad
# ─── Decimal para cálculos que deben ser exactos ─────────────────────────────
# Suma financiera con float — error acumulado
total_float = 0.0
for _ in range(10):
total_float += 0.1
print(total_float) # 0.9999999999999999 — no es 1.0
print(total_float == 1.0) # False
# La misma suma con Decimal — aritmética decimal exacta
total_decimal = Decimal('0')
for _ in range(10):
total_decimal += Decimal('0.1') # La cadena es clave: Decimal(0.1) importa el error de float
print(total_decimal) # 1.0
print(total_decimal == 1) # True
# Redondeo controlado — necesario en facturación, impuestos, etc.
precio = Decimal('19.995')
redondeado = precio.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(redondeado) # 20.00
# ─── Cuándo float sigue siendo la elección correcta ──────────────────────────
# Cálculo científico, ML, gráficos — la pequeña imprecisión es irrelevante
# y la velocidad de float supera con creces a Decimal
import math
print(math.sqrt(2)) # 1.4142135623730951 — más que suficiente para física o geometría
Lo que hace cada decisión
math.isclose(a, b) toma dos tolerancias: rel_tol (relativa, por defecto 1e-9) y abs_tol (absoluta, por defecto 0.0). El resultado es True si la diferencia es menor que max(rel_tol * max(|a|, |b|), abs_tol). La tolerancia relativa funciona bien para números grandes, pero falla cerca del cero — si estás comparando valores que pueden ser 0.0, añade siempre un abs_tol razonable.
Decimal('0.1') con comillas es esencial. Si escribes Decimal(0.1) sin comillas, le pasas el float ya impreciso y Decimal lo importa fielmente: Decimal(0.1) te da Decimal('0.1000000000000000055511151231257827021181583404541015625'). La cadena le dice a Decimal cuál es el número exacto que quieres representar.
float sigue siendo la herramienta correcta para la mayor parte del trabajo numérico. Es nativo en hardware, extremadamente rápido, y para ciencia, machine learning o geometría, los errores de representación son insignificantes respecto a la incertidumbre del problema real. Decimal tiene un coste significativo en rendimiento y solo tiene sentido cuando la exactitud decimal es un requisito de negocio explícito: dinero, contabilidad, impuestos.
La propiedad nan != nan no es un capricho — es parte del estándar IEEE 754 y tiene una razón lógica: si tienes un resultado indefinido, no puedes afirmar que dos indefinidos son el mismo indefinido. Por eso math.isnan() existe como función separada.
Errores que debes conocer
Error: usar Decimal(0.1) en lugar de Decimal('0.1') importa silenciosamente el error de representación del float.
# ❌ Wrong
from decimal import Decimal
x = Decimal(0.1)
print(x) # 0.1000000000000000055511151231257827021181583404541015625
# ✅ Right
x = Decimal('0.1')
print(x) # 0.1
El constructor acepta tanto float como cadena, pero con float ya recibes el valor aproximado — la cadena es la única forma de decirle el número exacto que quieres.
Error: comparar un float con float('nan') usando == siempre devuelve False, incluso si la variable contiene nan.
# ❌ Wrong
import math
valor = float('nan')
if valor == float('nan'): # Nunca es True
print("es NaN")
# ✅ Right
if math.isnan(valor): # Correcto
print("es NaN")
nan no es igual a nada por definición del estándar; math.isnan() es el único test fiable.
Error: usar math.isclose() cerca del cero sin abs_tol da falsos negativos porque la tolerancia relativa se multiplica por valores muy pequeños.
# ❌ Wrong math.isclose(0.0, 0.0000001) # False — rel_tol * 0.0 = 0.0, umbral demasiado pequeño # ✅ Right math.isclose(0.0, 0.0000001, abs_tol=1e-6) # True — umbral absoluto explícito
Cuando alguno de los valores puede ser cero o estar muy cerca de cero, la tolerancia relativa sola no es suficiente.
N° 21