Abres Python, escribes 0.1 + 0.2 y esperas un 0.3. Python te contesta esto:
0.30000000000000004
Esta suma sale en la semana 1 de Python desde cero, mi curso gratis y en español, y la primera vez desconcierta. Lo primero que uno piensa es que algo se instaló mal, pero no es eso. La propia documentación de Python aclara que tampoco es un error del lenguaje ni de tu código. La explicación está en cómo la computadora guarda los números con punto.
El 0.1 no cabe en binario
Piensa en un tercio. En decimal se escribe 0.3333… y los treses no se terminan. Puedes escribir cien o mil, y sigue sin ser exactamente 1/3. El número existe, lo que pasa es que en base 10 no se puede escribir completo con una cantidad finita de dígitos.
Al 0.1 le pasa lo mismo, pero en binario, la base 2 con la que trabaja la computadora. En binario, un décimo es 0.000110011001100110011... y esa cola de 0011 tampoco se termina. Donde la cortes, te quedas con una aproximación.
O sea que la suma no está rota. El 0.1 que escribiste ya no cabía exacto desde antes de sumar.
¿Y dónde se corta? Desde al menos el año 2000, casi todas las computadoras usan el estándar IEEE 754 (del Institute of Electrical and Electronics Engineers, el Instituto de Ingenieros Eléctricos y Electrónicos), y casi todas guardan el float de Python en su formato de 64 bits, el de precisión doble. Ese formato trae 53 bits de precisión. Con esos 53 bits, el 0.1 queda guardado como esta fracción:
3602879701896397 / 2**55
Está muy cerca de un décimo, pero un poquito por encima. Si escribes ese valor completo en decimal, sale esto:
0.1000000000000000055511151231257827021181583404541015625Ese poquito es muy chiquito. Según la documentación, en la mayoría de las máquinas el error de cada operación no pasa de 1 parte en 2**53, y para casi todos los usos eso alcanza. El detalle es que cada operación puede sumar su propio redondeo.
Entonces, ¿por qué veo 0.1?
Porque Python no te enseña el decimal largo. Desde Python 3.1, en la mayoría de los sistemas, elige la escritura más corta que vuelve a dar el mismo valor guardado. Tú escribes 0.1 y ves 0.1, aunque adentro siga estando la fracción binaria.
Al sumar, los pedacitos de más se juntan y ya no hay una escritura corta que los esconda. Ahí aparece el 0.30000000000000004.
Con tres décimos pasa lo mismo. 0.1 + 0.1 + 0.1 == 0.3 da False, y si a esa suma le restas 0.3, el resultado no es cero: la página del módulo decimal lo muestra como 5.5511151231257827e-017.
Esto no es exclusivo de Python. Pasa en los lenguajes que usan el punto flotante del procesador, y algunos simplemente no te muestran la diferencia.
En el curso le digo "decimal" al float
Y eso tiene trampa.
En la tabla de la semana 1 puse el float como decimal, con ejemplos como 1.5 y 36.6, para medidas y precios. Lo llamé así por el punto, que el primer día es la pista que más ayuda a reconocerlo.
Pero Python tiene un módulo que se llama decimal, y es otra cosa. Ahí los números se guardan en base 10. Si escribes el décimo como texto, entre comillas, sí cabe exacto y la suma da 0.3:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # Decimal('0.3')
Con este módulo, la resta de los tres décimos que antes daba 5.55e-17 sí da cero.
Ojo con las comillas. Si armas un Decimal desde un float, Python convierte el valor binario tal cual, con todo y su cola. Decimal(3.14) no te da 3.14, te da 3.140000000000000124344978758017532527446746826171875. Y Decimal.from_float(0.1) te regresa el decimal largo de arriba, que no es lo mismo que Decimal("0.1").
¿Y el 1/3? Ese no cabe ni en binario ni en decimal. Para eso está el módulo fractions, que guarda el número como fracción y lo deja exacto.
Cuando el número es un precio
Para los ejercicios del curso basta con redondear al mostrar: round(numero, 2). La documentación de Python recomienda lo mismo para un uso casual, que es redondear lo que muestras del resultado final.
Lo que no funciona es redondear cada pieza antes de sumar. Este ejemplo sale de la documentación, y da False:
round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)Si lo que quieres saber es si dos resultados están cerca, para eso existe math.isclose. Con los tres décimos, math.isclose(0.1 + 0.1 + 0.1, 0.3) da True.
Un sistema de cobros de verdad ya es otra historia. Como dice el README del curso, ahí se suelen guardar los centavos como enteros, o se usa el módulo decimal, que trae quantize para redondear a una cantidad fija de dígitos (la documentación lo señala justo para aplicaciones de dinero). En la semana 1 eso puede esperar, y el registro de gastos llega hasta el proyecto final.
Dónde seguirlo
La versión corta, con el ejemplo de 0.1 + 0.2, está en las notas de la semana 1. El curso completo, gratis y en español, está en Python desde cero.
Fuentes
- Documentación de Python, aritmética de punto flotante: el
0.1como fracción binaria que no se termina, los 53 bits, la fracción3602879701896397 / 2**55, el decimal largo guardado, la impresión corta desde Python 3.1, que no es un error del lenguaje ni del código, IEEE 754 de 64 bits desde al menos el año 2000, el tope de 1 parte en2**53por operación, la comparación de tres décimos, elroundque sigue enFalse,math.isclose, el módulofractionsy redondear lo que se muestra al final. Consultada en la versión 3.15.2 de la documentación; el curso usa Python 3.12 o 3.13, y lo que aquí se describe delfloatvale igual para esas versiones - Módulo decimal: la resta de tres décimos (cero en decimal,
5.5511151231257827e-017en binario),Decimal(3.14)frente aDecimal('3.14'),Decimal.from_float(0.1)frente aDecimal('0.1')yquantizepara aplicaciones monetarias - Notas de la semana 1: el
floaten la tabla como decimal (1.5,36.6), el ejemplo0.1 + 0.2, que no es un fallo de la instalación yround(numero, 2)para mostrar centavos - README del curso: Python 3.12 y 3.13,
round(numero, 2)para los precios del curso, los centavos como enteros o el módulodecimalen un sistema de cobros real, y el registro de gastos como proyecto final - Python desde cero: el curso es gratis, en español y organizado por semanas