decimal.InvalidOperation ConversionSyntax (tested)
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>] usually means Decimal() got a string like '1,000.50'. We reproduced it and three other causes on Python 3.13.5, and found that except ValueError doesn't catch it.
1. Decimal() got a string it can't parse
from decimal import Decimal
for s in ["19.99", " 3.5 ", "1,000.50", "$5", "", "nan"]:
try:
print(repr(s), "->", repr(Decimal(s)))
except Exception as e:
print(repr(s), "->", type(e).__name__, e)
Output:
'19.99' -> Decimal('19.99')
' 3.5 ' -> Decimal('3.5')
'1,000.50' -> InvalidOperation [<class 'decimal.ConversionSyntax'>]
'$5' -> InvalidOperation [<class 'decimal.ConversionSyntax'>]
'' -> InvalidOperation [<class 'decimal.ConversionSyntax'>]
'nan' -> Decimal('NaN')
Measured on Python 3.13.5: the bracketed [<class 'decimal.ConversionSyntax'>] is the whole message, and it means the string isn't a number Decimal understands. Thousands separators, currency signs and the empty string all fail. Surrounding whitespace is stripped and accepted, and "nan" is accepted silently as Decimal('NaN'), which then flows on into your totals.
2. except ValueError doesn't catch it
from decimal import Decimal, InvalidOperation
print(InvalidOperation.__mro__)
print(issubclass(InvalidOperation, ValueError))
try:
try:
Decimal("1,000.50")
except ValueError:
print("caught by except ValueError")
except InvalidOperation:
print("missed by except ValueError, caught by except InvalidOperation")
Output:
(<class 'decimal.InvalidOperation'>, <class 'decimal.DecimalException'>, <class 'ArithmeticError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)
False
missed by except ValueError, caught by except InvalidOperation
Measured: InvalidOperation is an ArithmeticError, not a ValueError, so the usual except ValueError you'd write around int() or float() lets it straight through. Catch decimal.InvalidOperation by name.
3. A parser that cleans input and rejects NaN
from decimal import Decimal, InvalidOperation
def parse_money(s):
cleaned = s.strip().replace(",", "").replace("$", "")
try:
d = Decimal(cleaned)
except InvalidOperation:
raise ValueError(f"not a number: {s!r}") from None
if not d.is_finite():
raise ValueError(f"not a finite number: {s!r}")
return d
for s in ["1,000.50", "$5", " 3.5 ", "nan", "", "abc"]:
try:
print(repr(s), "->", parse_money(s))
except ValueError as e:
print(repr(s), "-> ValueError:", e)
Output:
'1,000.50' -> 1000.50
'$5' -> 5
' 3.5 ' -> 3.5
'nan' -> ValueError: not a finite number: 'nan'
'' -> ValueError: not a number: ''
'abc' -> ValueError: not a number: 'abc'
Measured: strip separators and currency symbols before converting, turn InvalidOperation into a ValueError your callers already handle, and check is_finite() so "nan" and "inf" don't slip in. Only strip commas if your input never uses a comma as the decimal point.
4. quantize() on a number bigger than the precision
from decimal import Decimal, getcontext
print("precision:", getcontext().prec)
big = Decimal(10) ** 30
try:
print(big.quantize(Decimal("0.01")))
except Exception as e:
print(type(e).__name__, e)
print(Decimal("123.456").quantize(Decimal("0.01")))
Output:
precision: 28
InvalidOperation [<class 'decimal.InvalidOperation'>]
123.46
Measured: the default context has 28 significant digits. Rounding 1030 to 2 decimal places needs 33 digits, so quantize() raises InvalidOperation with no hint about why. Raise the precision (getcontext().prec = 50, or a local localcontext()) for very large amounts. More in our prec is significant digits guide.
5. 0/0 is InvalidOperation, 1/0 is not
from decimal import Decimal
for a, b in [("0", "0"), ("1", "0")]:
try:
print(a, "/", b, "=", Decimal(a) / Decimal(b))
except Exception as e:
print(a, "/", b, "->", type(e).__name__, "| ZeroDivisionError?", isinstance(e, ZeroDivisionError))
Output:
0 / 0 -> InvalidOperation | ZeroDivisionError? False
1 / 0 -> DivisionByZero | ZeroDivisionError? True
Measured: dividing zero by zero raises InvalidOperation, which is not a ZeroDivisionError. Dividing anything else by zero raises DivisionByZero, which is. An except ZeroDivisionError around a Decimal average misses the empty-total case.
6. None and floats are different errors
from decimal import Decimal
for v in [None, 0.1, "0.1"]:
try:
print(repr(v), "->", repr(Decimal(v)))
except Exception as e:
print(repr(v), "->", type(e).__name__, e)
Output:
None -> TypeError conversion from NoneType to Decimal is not supported
0.1 -> Decimal('0.1000000000000000055511151231257827021181583404541015625')
'0.1' -> Decimal('0.1')
Measured: Decimal(None) is a TypeError, not InvalidOperation. A float converts without any error but keeps the binary error of the float; pass a string instead. See our Decimal from float guide.
Frequently asked questions
What does decimal.InvalidOperation [<class 'decimal.ConversionSyntax'>] mean?
Decimal() was given a string it can't parse. We measured '1,000.50', '$5' and '' all raising it on Python 3.13.5; whitespace around a number is fine.
Why doesn't except ValueError catch decimal.InvalidOperation?
InvalidOperation subclasses ArithmeticError, not ValueError. We checked its MRO and an except ValueError that let it through. Catch decimal.InvalidOperation.
Why does quantize raise InvalidOperation?
The result needs more digits than the context precision (28 by default). We measured Decimal(10**30).quantize(Decimal('0.01')) raising it; raise prec to fix.
Is Decimal 0/0 a ZeroDivisionError?
No. We measured 0/0 raising InvalidOperation, which is not a ZeroDivisionError, while 1/0 raises DivisionByZero, which is.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5: Decimal() on six strings, the InvalidOperation MRO, a cleaning parser, quantize past precision 28, 0/0 vs 1/0, and None/float input. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; everything else here is built-in Python.