Home / Lenguajes / Números de punto flotante: cómo funcionan y cómo evitar errores
Números de punto flotante: cómo funcionan y cómo evitar errores

Números de punto flotante: cómo funcionan y cómo evitar errores

30 October 2026 · 17 min de lectura ··

El número que no suma lo que esperas

Casi todos los programadores se encuentran, tarde o temprano, con una línea de código que parece desafiar las matemáticas. Abren una consola, escriben 0.1 + 0.2 y el resultado no es 0.3, sino 0.30000000000000004. La primera reacción suele ser pensar en un error del lenguaje, pero el comportamiento es idéntico en Python, JavaScript, Java, C, Rust o Go. No se trata de un fallo, sino de una consecuencia directa de cómo los ordenadores almacenan los números con decimales.

Esos números reciben el nombre de números de punto flotante. Son la forma estándar de representar valores fraccionarios en el hardware moderno: precios, coordenadas, temperaturas, probabilidades, pesos de una red neuronal o la posición de un personaje en un videojuego. Entender cómo funcionan no es un detalle académico; es la diferencia entre un sistema de facturación que cuadra al céntimo y uno que acumula pequeñas discrepancias hasta que alguien las descubre en una auditoría.

En esta guía explicaremos qué es exactamente un número de punto flotante, por qué se llama así, cómo lo representa el estándar IEEE 754 y por qué aparecen esos resultados sorprendentes. Después pasaremos a la práctica: cómo comparar valores correctamente, cómo sumar sin perder precisión, qué tipos usar para el dinero y cómo se comportan los principales lenguajes, incluidas novedades recientes como Math.sumPrecise en ECMAScript 2026. Los ejemplos de Python son válidos para la versión estable actual, la 3.15, publicada en octubre de 2026.

Qué es un número de punto flotante

En el lenguaje cotidiano, un número de punto flotante es simplemente un número con parte decimal, positivo o negativo: 3.75, -0.002 o 1024.5. En programación, el término designa además un formato concreto de almacenamiento que permite representar tanto valores muy pequeños como valores enormes con una cantidad fija de memoria.

La clave está en el nombre. La coma decimal no ocupa una posición fija, sino que “flota”: se desplaza según la magnitud del número. Es la misma idea que la notación científica que se aprende en la escuela. El número 0.000042 puede escribirse como 4.2 × 10⁻⁵, y 6 020 000 como 6.02 × 10⁶. En ambos casos se guardan dos datos: las cifras significativas y el exponente que indica dónde va la coma.

Esto contrasta con los enteros, que no tienen parte fraccionaria y se almacenan de forma exacta dentro de su rango. Muchos lenguajes distinguen ambos tipos de manera explícita. En C, por ejemplo, la elección del tipo determina cómo se guarda el valor:

#include <stdio.h>

int main(void) {
    int unidades = 18;          /* entero exacto */
    float temperatura = 21.75f; /* punto flotante de 32 bits */
    double distancia = 384400.5; /* punto flotante de 64 bits */

    printf("%d %f %f\n", unidades, temperatura, distancia);
    return 0;
}
18 21.750000 384400.500000

Otros lenguajes simplifican el modelo. En JavaScript, el tipo number es siempre un número de punto flotante de 64 bits, incluso cuando escribes un valor sin decimales como 122. En Python, los literales sin punto decimal son int de precisión arbitraria y los que llevan punto son float, que internamente también son valores de 64 bits.

Cómo se guarda un número: el estándar IEEE 754

Prácticamente todo el hardware y los lenguajes actuales siguen el estándar IEEE 754, cuya revisión vigente es la de 2019. Este estándar define cómo se codifican los números de punto flotante en binario, cómo se redondean los resultados y cómo se representan casos especiales como el infinito.

Un número en formato IEEE 754 se divide en tres campos:

  • El signo: un bit que indica si el número es positivo o negativo.
  • El exponente: indica la potencia de dos por la que se multiplica la mantisa.
  • La mantisa, también llamada significando: contiene las cifras significativas del número.

El valor final se obtiene, de forma simplificada, como signo × mantisa × 2^exponente. Los dos formatos más utilizados son:

  • Precisión simple (32 bits): 1 bit de signo, 8 de exponente y 23 de mantisa. Ofrece unas 7 cifras decimales significativas. Es el float de C, C++ y Java.
  • Precisión doble (64 bits): 1 bit de signo, 11 de exponente y 52 de mantisa. Ofrece entre 15 y 17 cifras significativas. Es el double de C y Java, el float de Python y el number de JavaScript.

La diferencia entre precisión simple y doble se aprecia enseguida al imprimir el mismo valor con muchos decimales:

#include <stdio.h>

int main(void) {
    float simple = 0.1f;
    double doble = 0.1;

    printf("simple: %.20f\n", simple);
    printf("doble:  %.20f\n", doble);
    return 0;
}
simple: 0.10000000149011611938
doble:  0.10000000000000000555

Ninguno de los dos guarda exactamente 0.1. Ambos guardan el valor más cercano que su formato puede representar, y la versión de 64 bits simplemente se acerca mucho más.

En Python puedes inspeccionar los bits reales de un float con el módulo struct:

import struct

def bits_de(valor: float) -> str:
    (entero,) = struct.unpack(">Q", struct.pack(">d", valor))
    b = f"{entero:064b}"
    return f"{b[0]} {b[1:12]} {b[12:]}"

print(bits_de(0.1))
print(bits_de(-2.5))
0 01111111011 1001100110011001100110011001100110011001100110011010
1 10000000000 0100000000000000000000000000000000000000000000000000

En la primera línea se ve el patrón 1001 repitiéndose en la mantisa, y en el último tramo un redondeo. Esa repetición es la clave de todo lo que sigue.

Por qué 0.1 más 0.2 no da 0.3

Igual que en base diez el número un tercio no puede escribirse con una cantidad finita de cifras (0.3333...), en base dos hay muchas fracciones decimales que tampoco tienen una representación finita. 0.1, 0.2 y 0.3 están entre ellas. La representación binaria de decimales como estos es una secuencia periódica infinita, y el ordenador debe cortarla en algún punto.

Cuando escribes 0.1, el ordenador almacena en realidad el número binario más próximo que cabe en 52 bits de mantisa. Lo mismo ocurre con 0.2. Al sumarlos, los pequeños errores de cada uno se combinan, y el resultado se redondea de nuevo al valor representable más cercano, que no coincide con el valor representable más cercano a 0.3.

Python permite ver el valor exacto que guarda un float gracias al módulo decimal:

from decimal import Decimal

print(Decimal(0.1))
print(Decimal(0.2))
print(Decimal(0.1 + 0.2))
print(Decimal(0.3))
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000444089209850062616169452667236328125
0.299999999999999988897769753748434595763683319091796875

La suma queda ligeramente por encima de 0.3 y el valor almacenado para 0.3 queda ligeramente por debajo. Por eso la comparación directa falla:

print(0.1 + 0.2 == 0.3)
print(0.1 + 0.2)
False
0.30000000000000004

Es importante entender que el resultado no es aleatorio ni impreciso en el sentido de “poco fiable”. Las operaciones en punto flotante son deterministas y están perfectamente especificadas: el estándar garantiza que cada operación básica devuelve el valor representable más cercano al resultado exacto. Lo que ocurre es que el conjunto de valores representables no incluye todos los decimales que usamos a diario.

Valores especiales: infinito, NaN y el cero negativo

IEEE 754 reserva algunas combinaciones de bits para valores especiales que conviene conocer, porque aparecen en situaciones reales y tienen reglas propias.

El infinito positivo y negativo surgen cuando un resultado supera el rango representable o al dividir por cero en lenguajes que siguen el estándar al pie de la letra:

console.log(1 / 0);
console.log(-1 / 0);
console.log(Number.MAX_VALUE * 2);
Infinity
-Infinity
Infinity

NaN, siglas de Not a Number, representa el resultado de operaciones indefinidas, como restar infinito de infinito o calcular la raíz cuadrada de un número negativo. Su propiedad más llamativa es que no es igual a nada, ni siquiera a sí mismo:

import math

nan = float("nan")
print(nan == nan)
print(math.isnan(nan))
print(float("inf") - float("inf"))
False
True
nan

Por eso nunca debes comprobar si un valor es NaN con ==. Usa math.isnan en Python o Number.isNaN en JavaScript. En JavaScript, evita la función global isNaN, que convierte primero su argumento a número y devuelve true para cadenas como "hola".

El cero negativo es el valor especial menos conocido. Existe porque el bit de signo es independiente del resto, y se conserva en algunas operaciones:

console.log(-0 === 0);
console.log(Object.is(-0, 0));
console.log(1 / -0);
true
false
-Infinity

En la mayoría de los cálculos el cero negativo es inofensivo, pero puede aparecer en interfaces como un incómodo -0 al formatear coordenadas o variaciones porcentuales, y afecta a funciones como Math.atan2.

Límites, épsilon y enteros seguros

Cada formato tiene un rango y una resolución. La resolución depende de la magnitud: cerca de cero, los valores representables están muy juntos; con números grandes, se separan cada vez más. La distancia entre 1.0 y el siguiente número representable se conoce como épsilon de la máquina.

import sys

print(sys.float_info.epsilon)
print(sys.float_info.max)
print(sys.float_info.dig)
2.220446049250313e-16
1.7976931348623157e+308
15

En JavaScript, el mismo valor está disponible como Number.EPSILON. Junto a él existe otra constante crítica: Number.MAX_SAFE_INTEGER. Como la mantisa tiene 52 bits más uno implícito, los enteros solo se representan de forma exacta hasta 2⁵³ − 1. Por encima de ese valor, algunos enteros simplemente no existen:

console.log(Number.MAX_SAFE_INTEGER);
console.log(2 ** 53 === 2 ** 53 + 1);
console.log(9007199254740993);
9007199254740991
true
9007199254740992

Este límite causa errores reales cuando una API devuelve identificadores numéricos de 64 bits, como los que generan muchas bases de datos o servicios de mensajería. Si el identificador supera 2⁵³, al parsear el JSON en JavaScript se corrompe silenciosamente. Las soluciones habituales son transmitir esos identificadores como cadenas o usar BigInt:

const idPedido = 9007199254740993n;
console.log(idPedido + 1n);
9007199254740994n

ECMAScript 2026 añade además acceso al texto original en JSON.parse y la función JSON.rawJSON, que facilitan leer y escribir números grandes en JSON sin pasar por number.

Cómo comparar números de punto flotante

La regla fundamental es no usar igualdad exacta para resultados de cálculos. Para comparar números de punto flotante se comprueba si están lo bastante cerca, es decir, se usa una tolerancia.

Hay dos tipos de tolerancia. La absoluta acepta diferencias menores que un valor fijo, y funciona bien cuando trabajas con magnitudes conocidas. La relativa acepta diferencias proporcionales al tamaño de los números, y funciona mejor cuando los valores pueden ser grandes o pequeños. Python combina ambas en math.isclose:

import math

subtotal = 0.1 + 0.2
print(math.isclose(subtotal, 0.3))
print(math.isclose(1e-12, 0.0))
print(math.isclose(1e-12, 0.0, abs_tol=1e-9))
True
False
True

El segundo resultado sorprende a menudo: con la tolerancia relativa por defecto, ningún valor distinto de cero es “cercano” a cero, porque la diferencia relativa es del 100 %. Cuando compares con cero, indica siempre una tolerancia absoluta.

En JavaScript no hay una función equivalente incorporada, pero es sencillo escribirla:

function casiIguales(a, b, relTol = 1e-9, absTol = 0) {
    if (a === b) return true;
    const diferencia = Math.abs(a - b);
    return (
        diferencia <=
        Math.max(relTol * Math.max(Math.abs(a), Math.abs(b)), absTol)
    );
}

console.log(casiIguales(0.1 + 0.2, 0.3));
console.log(casiIguales(0.0000001, 0, 1e-9, 1e-6));
true
true

Un error frecuente es usar Number.EPSILON directamente como tolerancia absoluta. Solo es adecuado para valores cercanos a 1; con números como 1000 o 0.001, la distancia entre valores representables es muy diferente y esa tolerancia resulta demasiado estricta o demasiado laxa.

Errores de redondeo acumulados

Un error de una diezmilbillonésima parece irrelevante, y en una sola operación lo es. El problema aparece cuando se repiten millones de operaciones. Los errores de redondeo acumulados pueden crecer hasta afectar a cifras visibles:

total = 0.0
for _ in range(1_000_000):
    total += 0.1

print(total)
100000.00000133288

Existen técnicas para reducir este efecto. La más conocida es la suma compensada, que lleva un registro del error cometido en cada paso y lo corrige en el siguiente. Python ofrece desde hace tiempo math.fsum, que calcula la suma correctamente redondeada, y desde la versión 3.12 la función integrada sum también aplica un algoritmo de compensación cuando suma valores float:

import math

valores = [0.1] * 1_000_000
print(math.fsum(valores))
print(sum(valores))
100000.0
100000.0

En JavaScript, ECMAScript 2026 incorpora Math.sumPrecise, que recibe un iterable y devuelve la suma sin acumular errores intermedios. La diferencia es evidente cuando se mezclan magnitudes muy distintas:

const movimientos = [1e20, 0.1, -1e20];

const conReduce = movimientos.reduce((acc, x) => acc + x, 0);
const preciso = Math.sumPrecise(movimientos);

console.log(conReduce);
console.log(preciso);
0
0.1

Con reduce, el 0.1 se pierde por completo al sumarse a 1e20, porque a esa magnitud la resolución es mucho mayor que 0.1. Math.sumPrecise conserva el resultado correcto. Antes de usarlo en producción, comprueba que tus entornos de ejecución objetivo ya lo soportan.

Otra recomendación práctica es el orden de las operaciones. Sumar primero los valores pequeños y después los grandes reduce la pérdida de precisión, y restar dos números casi iguales, lo que se conoce como cancelación catastrófica, destruye cifras significativas. Reorganizar fórmulas para evitar esas restas es una técnica clásica del cálculo numérico.

Cómo manejar dinero en código

Si hay un ámbito donde los números de punto flotante causan problemas reales, es el dinero. Los importes monetarios son decimales exactos por definición, y los usuarios, los contables y los reguladores esperan que una factura cuadre al céntimo. La forma correcta de manejar dinero en código es evitar float y double para almacenar importes.

Hay dos estrategias principales. La primera es trabajar con enteros en la unidad mínima de la moneda, por ejemplo céntimos:

precio_centimos = 1999   # 19.99
cantidad = 3
total_centimos = precio_centimos * cantidad

print(f"{total_centimos // 100}.{total_centimos % 100:02d}")
59.97

Es rápido, exacto y funciona en cualquier lenguaje, aunque exige cuidado con monedas que tienen un número distinto de decimales y con cálculos de porcentajes que generan fracciones de céntimo.

La segunda estrategia es usar un tipo decimal de precisión configurable. En Python, el módulo decimal:

from decimal import Decimal, ROUND_HALF_EVEN

precio = Decimal("19.99")
iva = Decimal("0.16")

impuesto = (precio * iva).quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
total = precio + impuesto

print(impuesto, total)
3.20 23.19

Fíjate en que el valor se construye a partir de una cadena, Decimal("19.99"), y no de un float. Si escribes Decimal(19.99), heredas el error binario del float original. El método quantize fija el número de decimales y la política de redondeo, que debe elegirse según la normativa aplicable: el redondeo bancario ROUND_HALF_EVEN y el redondeo comercial ROUND_HALF_UP dan resultados distintos en los casos que terminan exactamente en cinco.

En Java, el equivalente es BigDecimal, con las mismas precauciones:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Factura {
    public static void main(String[] args) {
        BigDecimal precio = new BigDecimal("19.99");
        BigDecimal iva = new BigDecimal("0.16");
        BigDecimal impuesto = precio.multiply(iva).setScale(2, RoundingMode.HALF_EVEN);
        System.out.println(precio.add(impuesto));
    }
}
23.19

En bases de datos, los importes deben guardarse en columnas NUMERIC o DECIMAL con precisión y escala explícitas, nunca en REAL o DOUBLE PRECISION:

CREATE TABLE facturas (
  id      BIGSERIAL PRIMARY KEY,
  cliente TEXT NOT NULL,
  importe NUMERIC(12, 2) NOT NULL CHECK (importe >= 0)
);

Redondeo y formato al mostrar valores

Un problema distinto, aunque relacionado, es cómo se muestran los números. Muchas funciones de formato redondean el valor binario almacenado, no el decimal que escribiste, lo que produce resultados inesperados:

console.log((1.005).toFixed(2));
console.log((1.006).toFixed(2));
1.00
1.01

1.005 se almacena en realidad como 1.00499999999999989..., de modo que redondearlo a dos decimales da 1.00. Python muestra el mismo comportamiento con round(2.675, 2), que devuelve 2.67. Además, la función round de Python usa redondeo bancario, así que round(0.5) es 0 y round(1.5) es 2.

La recomendación es separar el cálculo de la presentación: calcula con el tipo adecuado y formatea solo al final. Para mostrar cantidades a usuarios, las APIs de internacionalización respetan además los separadores de cada país:

const formato = new Intl.NumberFormat("es-MX", {
    style: "currency",
    currency: "MXN",
});

console.log(formato.format(23.19));
$23.19

Si los valores proceden de un cálculo en céntimos o en Decimal, conviértelos al formato final en el último paso, y nunca vuelvas a leer una cadena formateada para seguir calculando con ella.

Más allá de float y double

Los formatos de 32 y 64 bits no son los únicos. El propio estándar IEEE 754 define otros, y el hardware y los lenguajes los han ido incorporando según las necesidades.

Los formatos de media precisión, de 16 bits, se utilizan mucho en gráficos y en aprendizaje automático, donde la memoria y el ancho de banda importan más que la precisión de cada valor. JavaScript incorporó en ECMAScript 2025 el tipo Float16Array y la función Math.f16round, que permiten trabajar con estos valores en el navegador:

const pesos = new Float16Array([0.1, 0.2, 0.3]);
console.log(Array.from(pesos));
[ 0.0999755859375, 0.199951171875, 0.300048828125 ]

En el extremo opuesto están los formatos de precisión extendida o cuádruple, útiles en cálculo científico, y los tipos decimales de punto flotante, que trabajan en base diez y representan exactamente valores como 0.1. El estándar C23 incluye tipos decimales opcionales como _Decimal64, y muchas bases de datos y lenguajes ofrecen sus propios tipos decimales.

Por último, existen alternativas que no son de punto flotante. Las fracciones exactas, como el módulo fractions de Python, representan números racionales sin pérdida alguna, a costa de rendimiento:

from fractions import Fraction

resultado = Fraction(1, 10) + Fraction(2, 10)
print(resultado, resultado == Fraction(3, 10))
3/10 True

Errores comunes y cómo evitarlos

Algunos fallos aparecen una y otra vez en código real.

El primero es comparar con == el resultado de un cálculo. Usa siempre una tolerancia adecuada a la magnitud de los datos.

El segundo es almacenar dinero en float o en columnas DOUBLE. Usa enteros en la unidad mínima o tipos decimales, y define en un único lugar la política de redondeo.

El tercero es crear decimales a partir de flotantes. Decimal(0.1) y new BigDecimal(0.1) arrastran el error binario; construye siempre desde cadenas.

El cuarto es usar números de punto flotante como contadores o claves. Un bucle que avanza de 0.1 en 0.1 hasta llegar a 1.0 puede ejecutarse una vez de más o de menos. Itera con enteros y calcula el valor decimal a partir del índice:

pasos = [i / 10 for i in range(11)]
print(pasos)
[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]

El quinto es ignorar NaN. Un solo NaN contamina cualquier operación en la que participe y hace que las comparaciones devuelvan false, lo que puede romper algoritmos de ordenación o filtros. Valida los datos de entrada y detecta NaN lo antes posible.

El sexto es perder precisión con identificadores grandes en JavaScript. Trata los identificadores de 64 bits como cadenas o como BigInt.

Solución de problemas: síntomas típicos

Cuando un cálculo da resultados extraños, estos síntomas ayudan a localizar la causa.

Si un total aparece como 59.970000000000006, el cálculo es correcto en punto flotante y el problema es de presentación o de tipo. Para importes, cambia a decimales; para datos científicos, formatea con el número de cifras adecuado.

Si una prueba unitaria falla con un mensaje como este:

AssertionError: 0.30000000000000004 != 0.3

sustituye la aserción de igualdad por una aproximada. En pytest existe pytest.approx, y en unittest el método assertAlmostEqual:

import pytest

def test_suma_descuentos():
    assert 0.1 + 0.2 == pytest.approx(0.3)

Si un resultado se convierte en NaN o Infinity de forma inesperada, busca divisiones por cero, raíces de negativos o logaritmos de valores no positivos, y comprueba si alguna entrada llegó vacía o mal parseada.

Si dos máquinas o dos compilaciones producen resultados ligeramente distintos, revisa las opciones de optimización. Algunas, como las de matemáticas rápidas de los compiladores de C y C++, permiten reordenar operaciones y romper las garantías de IEEE 754. También puede influir el uso de instrucciones de multiplicación y suma fusionadas, que redondean una vez en lugar de dos.

Casos de uso reales

En gráficos 3D y videojuegos, los números de precisión simple son la norma porque las GPU los procesan de forma masiva y la precisión es suficiente para la pantalla. Los problemas aparecen en mundos muy grandes, donde las coordenadas lejanas del origen pierden resolución y los objetos vibran; la solución habitual es desplazar el origen con el jugador.

En aprendizaje automático, el entrenamiento y la inferencia usan cada vez más formatos de 16 bits o incluso menores para reducir memoria y consumo, aplicando técnicas de escalado para compensar la menor precisión.

En aplicaciones financieras, los importes se guardan como decimales o enteros, mientras que los modelos de riesgo y las simulaciones estadísticas, donde importan las tendencias y no el céntimo exacto, sí utilizan punto flotante de doble precisión.

En sistemas de geolocalización, la doble precisión permite representar coordenadas con precisión submilimétrica, mientras que la precisión simple se queda en un margen de metros, insuficiente para muchas aplicaciones de navegación.

Conclusiones

Los números de punto flotante son una de las herramientas más útiles de la informática: permiten representar con una cantidad fija de memoria valores que van desde lo microscópico hasta lo astronómico. Su contrapartida es que no pueden representar exactamente todos los decimales, y que cada operación introduce un pequeño redondeo.

Entender el estándar IEEE 754, la diferencia entre precisión simple y doble, los valores especiales y los límites de los enteros seguros permite anticipar los problemas en lugar de descubrirlos en producción. A partir de ahí, las reglas prácticas son claras: compara con tolerancia, suma con algoritmos compensados como math.fsum o Math.sumPrecise, guarda el dinero en tipos decimales o en enteros, construye los decimales desde cadenas y separa siempre el cálculo de la presentación.

El famoso 0.30000000000000004 deja de ser un misterio en cuanto se entiende su origen. Y elegir el tipo numérico adecuado para cada problema es una de esas decisiones pequeñas que distinguen el código que funciona del código que se puede confiar.