Normalize mixed numeric inputs with singledispatch

Normalize mixed numeric inputs with singledispatch

functools.singledispatch chooses an implementation from the runtime type of its first argument. That makes it useful when a boundary accepts several numeric representations but must produce one representation. Here, normalize_amount returns a Decimal rounded to cents for Decimal, int, and finite float inputs.

The Decimal branch retains a decimal input directly. The float branch first applies str(value), then constructs Decimal from that display form; constructing Decimal directly from a float would capture the float’s binary approximation instead. Each supported route calls quantize(CENTS), where CENTS is Decimal("0.01"). The values Decimal("7.004"), 7, and 7.006 therefore normalize to 7.00, 7.00, and 7.01.

The equality assertion checks the resulting numeric values. The following exponent assertion separately verifies that every result is represented at two decimal places, avoiding the false assumption that Decimal equality alone preserves trailing-zero scale. A dedicated bool registration rejects booleans even though bool is a subclass of int. Non-finite floats and Decimals are rejected, while types that do not match a registered type or registered abstract base class reach the default handler. A subclass of a registered type can use that type’s implementation through method-resolution-order lookup. The default context’s rounding mode governs half-cent cases.

AI assistance disclosure: This article was prepared with AI assistance.

Example

from decimal import Decimal
from functools import singledispatch
from math import isfinite

CENTS = Decimal("0.01")


@singledispatch
def normalize_amount(value):
    raise TypeError(f"unsupported amount type: {type(value).__name__}")


@normalize_amount.register
def _(value: Decimal):
    if not value.is_finite():
        raise ValueError("amount must be finite")
    return value.quantize(CENTS)


@normalize_amount.register
def _(value: int):
    return Decimal(value).quantize(CENTS)


@normalize_amount.register
def _(value: float):
    if not isfinite(value):
        raise ValueError("amount must be finite")
    return Decimal(str(value)).quantize(CENTS)


@normalize_amount.register
def _(value: bool):
    raise TypeError("boolean values are not amounts")


amounts = [normalize_amount(Decimal("7.004")), normalize_amount(7), normalize_amount(7.006)]
assert amounts == [Decimal("7.00"), Decimal("7.00"), Decimal("7.01")]
assert all(amount.as_tuple().exponent == -2 for amount in amounts)
print(*amounts)

Expected output

7.00 7.00 7.01

Source: Python functools.singledispatch documentation.

Comments

Popular posts from this blog

Compare Two Text Files in Python Without Modifying Them

Seven CSV Quality Checks to Run Before Importing Data

Explain why SQLite transactions cannot make an HTTP call atomic