Decimal en Python: cálculos precisos

Publicado el: 30/07/2026
Tempo de leitura: 7 minutos
Calculadora y documentos que representan cálculos Decimal precisos en Python

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)  # False

No 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") * cantidad

Python 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)  # 314

Significancia 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.5600

El 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.33

Usa 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))    # 3

Muchas 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 restaurado

Es 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 FloatOperation

Esta 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  # TypeError

Los 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 FloatOperation en 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Código digital que representa identificadores UUID únicos y ordenables en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    uuid en Python: IDs únicos y ordenables

    Aprende uuid en Python: versiones 4, 5, 6 y 7, validación, almacenamiento, IDs ordenables y buenas prácticas de seguridad.

    Ler mais

    Tempo de leitura: 7 minutos
    30/07/2026
    Archivos organizados que representan almacenamiento temporal seguro en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    tempfile en Python: archivos temporales

    Aprende tempfile en Python para crear archivos y carpetas temporales seguros, con limpieza automática en Windows y Unix.

    Ler mais

    Tempo de leitura: 8 minutos
    29/07/2026
    Relojes que representan zonas horarias internacionales con zoneinfo en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    zoneinfo en Python: zonas horarias

    Aprende zoneinfo en Python para convertir zonas, tratar horario de verano, fold, UTC y tzdata sin errores de programación.

    Ler mais

    Tempo de leitura: 7 minutos
    29/07/2026
    Icono de documentos duplicados que representa copias superficiales y profundas en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    copy en Python: copia superficial y profunda

    Aprende copy en Python para crear copias superficiales, profundas y reemplazar campos sin compartir objetos mutables por error.

    Ler mais

    Tempo de leitura: 7 minutos
    28/07/2026
    Monitor con búsqueda binaria y listas ordenadas en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    bisect en Python: listas siempre ordenadas

    Aprende bisect en Python para mantener listas ordenadas, localizar rangos, encontrar vecinos e insertar valores con búsqueda binaria.

    Ler mais

    Tempo de leitura: 9 minutos
    27/07/2026
    Desarrollador implementando una cola de prioridad con heapq en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    heapq en Python: colas de prioridad

    Aprende heapq en Python para crear colas de prioridad, encontrar valores mínimos y procesar tareas con heaps eficientes.

    Ler mais

    Tempo de leitura: 7 minutos
    26/07/2026