decimal: compare_total() Sorts NaN, Regular sorted() Raises (tested)
Earlier we measured that Decimal's regular comparison operators raise InvalidOperation the moment a NaN is involved, which means sorted() can't be used at all on data that might contain one. We measured whether Decimal.compare_total() -- a method most people never reach for -- actually solves that, and what else it does differently from regular comparison.
1. Regular sorted() raises the instant a NaN is in the list
from decimal import Decimal
vals = [Decimal("3"), Decimal("NaN"), Decimal("1")]
sorted(vals)
Traceback (most recent call last):
...
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
Measured: sorted() on a list that includes a NaN raises InvalidOperation -- regular comparison operators (<, >) are undefined for NaN by design, per IEEE 754, so any sort that needs to compare it fails rather than silently placing it somewhere.
2. compare_total() gives every value a defined position, NaN included
from decimal import Decimal
import functools
vals = [Decimal("3"), Decimal("NaN"), Decimal("1")]
print(sorted(vals, key=functools.cmp_to_key(lambda a, b: a.compare_total(b))))
[Decimal('1'), Decimal('3'), Decimal('NaN')]
Measured: using compare_total() (via functools.cmp_to_key, since it's a three-way comparator, not an operator) sorts the same list successfully, placing NaN at the end with no error. compare_total() implements the IEEE 754 total ordering predicate, which guarantees every two values have a defined order -- including NaN vs. a number, and (though not shown here) NaN vs. another NaN by payload.
3. compare_total() also distinguishes equal-valued Decimals that == treats as the same
from decimal import Decimal
a = Decimal("1")
b = Decimal("1.0")
print("a == b:", a == b)
print("a.compare_total(b):", a.compare_total(b))
a == b: True
a.compare_total(b): 1
Measured: Decimal('1') == Decimal('1.0') is True -- they represent the same numeric value. But compare_total() returns 1 (not 0), because total ordering also accounts for representation (coefficient/exponent), not just numeric value -- two Decimals that are mathematically equal but stored with different exponents are NOT total-order-equal. If code switches from ==/sorted() to compare_total() purely to handle NaN, it should expect this second, unrelated behavior change too: values that used to compare equal may now sort into a defined but non-zero relative order.
Frequently asked questions
How do I sort a list of Decimal values that might contain NaN?
Use Decimal.compare_total() as the comparator, e.g. sorted(vals, key=functools.cmp_to_key(lambda a, b: a.compare_total(b))) -- we measured this succeeding where a plain sorted() call raises InvalidOperation the moment it needs to compare a NaN.
Why does sorted() raise InvalidOperation on a list of Decimals?
Because regular comparison operators are undefined for NaN per IEEE 754, and we confirmed Python's decimal module raises decimal.InvalidOperation rather than silently picking an order when a NaN needs to be compared during a sort.
Does Decimal.compare_total() give the same ordering as == and sorted()?
Not exactly -- we measured Decimal('1').compare_total(Decimal('1.0')) returning 1 (not equal) even though Decimal('1') == Decimal('1.0') is True. compare_total() implements IEEE 754 total ordering, which also considers a value's stored representation (exponent/coefficient), not just its numeric value.
Can compare_total() sort NaN relative to regular numbers?
Yes -- we measured a list containing Decimal('3'), Decimal('NaN'), and Decimal('1') sorting successfully with compare_total() as the comparator, placing NaN in a defined position (last, in our test) with no exception.
Is compare_total() a drop-in replacement for sorted() on Decimal lists?
Only if you also expect its secondary effect: it distinguishes equal-value Decimals with different representations (like Decimal('1') vs Decimal('1.0')) as non-equal, which == and ordinary sorted() do not. Switching to it purely to tolerate NaN changes that unrelated behavior too.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in decimal module: sorted a list of Decimal values containing a NaN with the regular sorted() builtin, then repeated it using Decimal.compare_total() as the sort key, and compared Decimal('1').compare_total(Decimal('1.0')) against the regular == result for the same pair. PlainHeaders is our extension; Python's decimal module is built in.