El dinero, los impuestos, las tasas, las mediciones y las reglas contables requieren resultados predecibles. El tipo float es rápido y adecuado para muchas tareas científicas, pero representa números en base binaria. Por eso, fracciones decimales simples como 0,1 no siempre se almacenan exactamente. El módulo Decimal en Python ofrece aritmética decimal con precisión configurable, reglas explícitas de redondeo y mecanismos para detectar operaciones inexactas.
Esta guía explica la construcción correcta, quantize(), contextos, modos de redondeo, traps, validación de entrada, serialización en APIs y almacenamiento en bases de datos. Complementa nuestros artículos sobre floats en Python, números y monedas con f-strings, tipos de datos, UUIDs y copias de objetos.
Por qué float puede sorprender
El punto flotante binario almacena muchos valores decimales como aproximaciones:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.1 + 0.1 == 0.3) # FalseNo es un error exclusivo de Python. El tutorial oficial sobre punto flotante explica que 0,1 tiene una expansión infinita en base 2, como 1/3 la tiene en base 10. El equipo guarda la fracción binaria representable más cercana.
Para simulaciones y gráficos, el error suele ser aceptable. Para contabilidad, precios e invariantes estrictos de igualdad decimal, la aritmética en base 10 suele representar mejor el problema.
Crear Decimal desde cadenas
Importa Decimal y pasa una cadena con el valor pretendido:
from decimal import Decimal
precio = Decimal("19.90")
tasa = Decimal("0.075")
total = precio * (Decimal("1") + tasa)
print(total)La cadena conserva exactamente los dígitos introducidos, incluidos los ceros finales significativos. Es la construcción preferida para formularios, JSON textual, configuraciones y valores de bases de datos.
Evita construir Decimal desde float
Pasar un float convierte exactamente la aproximación binaria almacenada:
from decimal import Decimal
print(Decimal(0.1))
# 0.100000000000000005551115123125782...Esto sirve para inspeccionar el valor real de un float, pero no representa el decimal pretendido por el usuario. Prefiere Decimal("0.1"). Si otra biblioteca entrega float y tu política consiste en usar su forma decimal mostrada, convierte mediante str() y documenta la decisión:
valor = Decimal(str(0.1))Enteros y Decimal.from_number()
Los enteros se convierten exactamente:
cantidad = Decimal(3)
subtotal = Decimal("9.90") * cantidadPython 3.14 añade Decimal.from_number(), que acepta int, float u otro Decimal, pero no cadenas. Un float sigue convirtiéndose con todos los dígitos de su equivalente binario.
valor = Decimal.from_number(314)
print(valor) # 314Significancia y ceros finales
Decimal conserva ceros finales para comunicar significancia:
from decimal import Decimal
print(Decimal("1.30") + Decimal("1.20")) # 2.50
print(Decimal("1.30") * Decimal("1.20")) # 1.5600El valor numérico 2,50 es igual a 2,5, pero la representación puede indicar dos posiciones decimales. Esta característica resulta útil en informes financieros y mediciones.
Redondear con quantize()
quantize() redondea un número al exponente de otro Decimal. Para dos posiciones:
from decimal import Decimal, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
valor = Decimal("7.325")
resultado = valor.quantize(CENTAVO, rounding=ROUND_HALF_UP)
print(resultado) # 7.33Usa una constante para la escala. El redondeo debe ocurrir donde la regla de negocio lo exige: por artículo, por línea, por impuesto o solo en el total. Redondear en etapas distintas puede producir totales diferentes.
ROUND_HALF_EVEN frente a ROUND_HALF_UP
El contexto predeterminado usa ROUND_HALF_EVEN, conocido como redondeo al par. Los empates exactos eligen el último dígito par y reducen el sesgo acumulado.
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
valor = Decimal("2.5")
print(valor.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN)) # 2
print(valor.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) # 3Muchas reglas comerciales exigen ROUND_HALF_UP; otras requieren truncamiento, techo o piso. Sigue la legislación o especificación del dominio.
Otros modos de redondeo
El módulo ofrece ROUND_DOWN, ROUND_UP, ROUND_FLOOR, ROUND_CEILING, ROUND_HALF_DOWN y ROUND_05UP. La diferencia entre down y floor aparece con negativos: down se aproxima a cero y floor a menos infinito.
Prueba valores positivos, negativos y empates. Un conjunto que solo cubre 1,235 puede ocultar un fallo para -1,235.
El contexto decimal
La documentación oficial de Decimal organiza el módulo en números, contextos y señales. El contexto define precisión, redondeo, límites de exponente, flags y traps.
from decimal import Decimal, getcontext
ctx = getcontext()
print(ctx.prec) # 28 por defecto
print(ctx.rounding)
ctx.prec = 10
print(Decimal(1) / Decimal(7))La precisión significa dígitos significativos de las operaciones, no posiciones decimales del resultado. La construcción desde cadena conserva los dígitos; el contexto actúa durante los cálculos.
Usar localcontext() para cambios temporales
Cambiar el contexto activo dentro de código reutilizable puede sorprender a quien llama. Usa localcontext() para aislar una operación:
from decimal import Decimal, localcontext
with localcontext(prec=50) as ctx:
resultado = Decimal(1) / Decimal(7)
print(resultado)
# contexto anterior restauradoEs ideal para una etapa de alta precisión sin afectar el resto de la aplicación. Python 3.11 y posteriores aceptan atributos como argumentos con nombre.
Señales y flags persistentes
Las operaciones pueden señalar Inexact, Rounded, DivisionByZero, InvalidOperation, Overflow y Underflow. Las flags permanecen activas hasta limpiarlas.
from decimal import Decimal, getcontext
ctx = getcontext()
ctx.clear_flags()
Decimal(1) / Decimal(7)
print(ctx.flags)Las flags permiten auditar un lote sin detenerlo. Límpialas antes del cálculo, procesa y revisa las condiciones al terminar.
Las traps convierten señales en excepciones
Una trap activada hace que la señal lance una excepción. Es útil cuando un proceso no puede aceptar redondeo silencioso.
from decimal import Decimal, Inexact, localcontext
with localcontext() as ctx:
ctx.traps[Inexact] = True
try:
Decimal("1") / Decimal("3")
except Inexact:
print("No se permite un resultado inexacto")Las traps sirven en validaciones financieras, importaciones y pruebas. Captura excepciones concretas y presenta un error claro.
Detectar uso accidental de float
La señal FloatOperation impide que floats entren accidentalmente en un flujo decimal.
from decimal import Decimal, FloatOperation, localcontext
with localcontext() as ctx:
ctx.traps[FloatOperation] = True
Decimal(3.14) # lanza FloatOperationEsta protección obliga al equipo a definir conscientemente cómo convertir cada fuente.
Ejemplo de precio, descuento e impuesto
from decimal import Decimal, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
precio = Decimal("149.90")
cantidad = Decimal("3")
descuento = Decimal("0.10")
impuesto = Decimal("0.075")
subtotal = precio * cantidad
tras_descuento = subtotal * (Decimal("1") - descuento)
total = tras_descuento * (Decimal("1") + impuesto)
total = total.quantize(CENTAVO, rounding=ROUND_HALF_UP)
print(total)Una aplicación real debe definir si descuento e impuesto se aplican por artículo o sobre el agregado. Decimal ejecuta la regla; no decide la norma contable.
Porcentajes y tasas
Convierte 7,5% en Decimal("0.075"). No dividas un float por 100 antes de convertir. Para texto localizado, normaliza separadores con un parser explícito y rechaza formatos ambiguos.
def porcentaje(texto: str) -> Decimal:
valor = Decimal(texto.replace(",", "."))
return valor / Decimal("100")Este helper simple no debe aceptar separadores de miles o símbolos monetarios sin validación de locale.
Comparaciones y tipos mezclados
Sumar Decimal y float genera TypeError, evitando mezcla silenciosa:
Decimal("1.2") + 1.2 # TypeErrorLos enteros funcionan normalmente. Las comparaciones con floats están permitidas, pero reflejan la aproximación binaria. En un dominio decimal, normaliza ambos operandos.
NaN, Infinity y cero con signo
Decimal admite valores especiales:
from decimal import Decimal
valores = [
Decimal("NaN"),
Decimal("Infinity"),
Decimal("-Infinity"),
Decimal("-0"),
]No permitas que entren en precios o saldos sin política explícita. Valida con is_finite(), is_nan() e is_infinite().
Serialización JSON
El módulo json estándar no serializa Decimal automáticamente. Convertir a float reintroduce aproximación. Las APIs financieras suelen enviar una cadena:
registro = {
"total": str(Decimal("19.90")),
"currency": "EUR",
}Documenta el contrato para que el consumidor interprete el campo como decimal.
Almacenamiento en base de datos
Usa columnas DECIMAL o NUMERIC con precisión y escala definidas. No almacenes dinero en una columna binaria FLOAT. Los ORMs suelen mapear estos tipos a Decimal.
Valida límites antes de insertar. Una columna NUMERIC(10,2) no admite cualquier magnitud y el banco puede redondear o rechazar.
Rendimiento
Decimal suele ser más lento que float porque ofrece precisión y políticas adicionales. No reemplaces todos los floats. Usa float en gráficos, física y cálculo tolerante; Decimal en valores decimales exactos y redondeo regulado.
Mide procesos grandes. Guardar centavos como enteros puede ser más simple cuando moneda y escala son fijas. Intereses, cambio y cantidades fraccionarias suelen beneficiarse de Decimal.
Probar cálculos decimales
Las pruebas deben cubrir empates, negativos, límites, entrada inválida y orden de operaciones.
from decimal import Decimal, ROUND_HALF_UP
assert Decimal("2.675").quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
) == Decimal("2.68")Compara Decimal con Decimal, no con float. Prueba también totales de cuotas, impuestos y reconciliación con valores persistidos.
Errores frecuentes
- Usar
Decimal(0.1)esperando 0,1 exacto. - Redondear solo al mostrar cuando la norma exige redondeo real.
- Cambiar el contexto activo dentro de una función reutilizable.
- Mezclar Decimal y float.
- Convertir a float antes de serializar.
- Elegir el modo de redondeo sin consultar la regla.
- Ignorar flags de inexexactitud.
- Usar Decimal en todo sin medir rendimiento.
Buenas prácticas
- Crea valores desde cadenas o enteros.
- Centraliza constantes de escala y modos de redondeo.
- Usa
localcontext()para precisión temporal. - Activa
FloatOperationen módulos críticos. - Exige valores finitos.
- Almacena en columnas DECIMAL/NUMERIC.
- Serializa como cadena cuando debe conservarse precisión.
- Prueba reglas financieras con ejemplos autorizados.
Conclusión
El módulo Decimal en Python aporta controles que el punto flotante binario no fue diseñado para ofrecer: representación decimal exacta, ceros significativos, precisión configurable, modos explícitos de redondeo y señales auditables.
El uso correcto comienza en la entrada: construye desde cadenas, define la escala con quantize(), aísla contextos e impide mezclas accidentales con float. Cuando reglas de negocio, APIs y columnas de base comparten el mismo contrato decimal, dinero, tasas y mediciones se vuelven reproducibles, comprobables y mucho menos propensos a diferencias de céntimos.







