Para calcular dinero en Python, crea valores Decimal desde cadenas, establece una regla de redondeo y normaliza el resultado con quantize(). Esa es la respuesta directa: Decimal("19.90") conserva el valor decimal indicado, mientras que un float suele almacenar una aproximación binaria. Una diferencia diminuta puede impedir la conciliación de una factura o acumularse en miles de operaciones.
El módulo decimal forma parte de la biblioteca estándar y no requiere instalación. Ofrece precisión configurable, modos de redondeo y señales para detectar condiciones excepcionales. Sin embargo, no decide las reglas del negocio: moneda, escala, momento del redondeo y valores negativos deben definirse. Para repasar la base, consulta variables y tipos de datos en Python.
Por qué float es arriesgado para representar dinero
Un float usa punto flotante binario. Muchas fracciones breves en base diez no tienen una representación binaria finita, así que se guarda el valor representable más cercano:
print(0.1 + 0.2)
## 0.30000000000000004
No es un fallo de Python, sino una propiedad que explica el tutorial oficial sobre punto flotante. Los floats son adecuados para mediciones científicas y gráficos. El dinero exige dígitos decimales y redondeos repetibles, por lo que Decimal representa mejor esa intención.
from decimal import Decimal
subtotal = Decimal("0.10") + Decimal("0.20")
print(subtotal) # 0.30
print(subtotal == Decimal("0.30")) # True
Construye siempre desde texto. Decimal(0.1) importa la aproximación que ya contenía el float y muestra muchas cifras. Conserva como texto los datos de formularios, CSV o API hasta validarlos. La guía de manejo de excepciones en Python ayuda a rechazar entradas incorrectas sin ocultar errores.
Redondeo monetario explícito con quantize
quantize() adapta un valor al exponente de una plantilla. Para una moneda con dos decimales, usa Decimal("0.01"). ROUND_HALF_UP aleja los empates de cero y es habitual en reglas comerciales. ROUND_HALF_EVEN, predeterminado en el contexto, reduce el sesgo en series largas. La legislación o la política contable debe determinar cuál corresponde.
from decimal import Decimal, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
def como_dinero(valor: Decimal) -> Decimal:
return valor.quantize(CENTAVO, rounding=ROUND_HALF_UP)
precio = Decimal("37.95")
cantidad = 3
descuento = Decimal("0.10")
total = como_dinero(precio cantidad (Decimal("1") - descuento))
print(total) # 102.47
Redondea en el evento definido por el dominio. Redondear cada línea antes de sumar puede diferir de sumar importes precisos y redondear una vez la factura. Las dos políticas existen, pero no son intercambiables. Documenta la elegida y prueba valores de empate. La guía de pruebas unitarias con unittest permite convertir ejemplos contables en pruebas de regresión.
Precisión, contexto y resultados intermedios
El contexto decimal controla dígitos significativos, redondeo, límites y señales. Precisión no equivale al número de decimales de la moneda. Una división para calcular intereses puede necesitar muchas cifras intermedias, aunque el asiento final tenga dos. Usa localcontext() cuando una rutina necesite configuración propia y evita cambiar el comportamiento global.
from decimal import Decimal, localcontext, ROUND_HALF_EVEN
capital = Decimal("12500.00")
tasa_anual = Decimal("0.0875")
with localcontext() as ctx:
ctx.prec = 28
ctx.rounding = ROUND_HALF_EVEN
interes_diario = capital * tasa_anual / Decimal("365")
interes_contabilizado = interes_diario.quantize(Decimal("0.01"))
print(interes_contabilizado) # 3.00
La documentación oficial de decimal describe contextos, traps y modos. La motivación del diseño está en la PEP 327. En servicios grandes, concentra escala y política de redondeo en un módulo de dominio pequeño, en lugar de repetir constantes en cada controlador.
Caso real: líneas, impuesto y total de una factura
Una factura recibe precios externos, multiplica cantidades y aplica un impuesto. Este ejemplo valida texto, evita mezclar representaciones y devuelve importes normalizados:
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
def entrada_monetaria(texto: str) -> Decimal:
try:
valor = Decimal(texto)
except InvalidOperation as error:
raise ValueError("importe monetario inválido") from error
if not valor.is_finite() or valor < 0:
raise ValueError("el importe debe ser finito y no negativo")
return valor
def calcularfactura(elementos, tasa: str):
elementos = list(elementos)
for , cantidad in elementos:
if type(cantidad) is not int or cantidad <= 0:
raise ValueError("la cantidad debe ser un entero positivo")
subtotal = sum(
(entrada_monetaria(precio) cantidad for precio, cantidad in elementos),
start=Decimal("0"),
)
impuesto = (subtotal entrada_monetaria(tasa)).quantize(
CENTAVO, rounding=ROUND_HALF_UP
)
subtotal = subtotal.quantize(CENTAVO, rounding=ROUND_HALF_UP)
total = (subtotal + impuesto).quantize(CENTAVO, rounding=ROUND_HALF_UP)
return subtotal, impuesto, total
print(calcular_factura([("19.90", 2), ("5.35", 1)], "0.075"))
(Decimal('45.15'), Decimal('3.39'), Decimal('48.54'))
La validación rechaza NaN e infinito. Decimal los admite, pero normalmente no tienen sentido en pagos. Un sistema internacional también debe guardar el código ISO: sumar 10 EUR y 10 USD es un error de dominio aunque los números sean compatibles. Para persistir, elige una columna decimal con precisión y escala suficientes. La guía de SQLite con Python introduce estas decisiones.
Errores frecuentes
- Construir desde float: usa
Decimal("2.50")o texto validado. - Mezclar Decimal y float: mantén una representación en todo el flujo monetario.
- Redondear sin política: indica escala y modo mediante
quantize(). - Redondear demasiado pronto: conserva precisión hasta el evento contable acordado.
- Ignorar la moneda: Decimal representa una cantidad, no EUR, USD ni centavos.
- Enviar números JSON: el consumidor puede convertirlos a float; muchas API financieras usan cadenas.
Checklist para producción
- Recibe importes decimales como texto y valida formato, signo y límites.
- Define moneda, escala y modo de redondeo como política del dominio.
- Evita floats durante todo el recorrido del cálculo monetario.
- Conserva precisión intermedia y redondea en momentos documentados.
- Prueba empates, valores grandes, cero, negativos permitidos y repartos.
- Persiste con un decimal adecuado o unidades menores enteras cuando proceda.
- Guarda componentes del cálculo para auditoría y conciliación.
Las reglas legibles son más fáciles de auditar. Las prácticas de Clean Code en Python se aplican aquí: nombres como tasa, subtotal e impuesto son más seguros que una fórmula inexplicada. Precisión aritmética y mantenibilidad son requisitos diferentes; un sistema fiable necesita ambos.
Preguntas frecuentes
¿Decimal siempre es mejor que float?
No. Float funciona bien para ciencia, gráficos y mediciones aproximadas. Decimal conviene cuando la representación decimal exacta y el redondeo del negocio son requisitos, especialmente con dinero.
¿Puedo almacenar dinero como centavos enteros?
Sí, en dominios de escala fija. Los enteros son exactos, pero cambio de divisas, intereses, repartos y monedas con escalas distintas requieren reglas adicionales. Decimal suele expresar esos casos mejor.
¿Qué modo de redondeo debo elegir?
El exigido por contabilidad o legislación. ROUND_HALF_UP es común en comercio y ROUND_HALF_EVEN reduce el sesgo estadístico. Documenta la decisión y prueba los empates.
¿Decimal garantiza que un sistema financiero sea correcto?
No por sí solo. Resuelve la aritmética decimal, pero también hay que validar monedas, decidir cuándo redondear, persistir con seguridad, controlar concurrencia y conservar auditoría.