[Python-ideas] Should decimal.InvalidOperation subclass ValueError?

Case Van Horsen casevh at gmail.com
Wed May 25 03:22:21 EDT 2016

On Tue, May 24, 2016 at 5:26 PM, Nick Coghlan <ncoghlan at gmail.com> wrote:
> On 25 May 2016 at 02:56, Guido van Rossum <gvanrossum at gmail.com> wrote:
>> If it's the right thing to do we should do it, and consider how to do
>> it without breaking too much code too soon (or without clear
>> indication of failure). I don't think a PEP is needed unless there's a
>> lot of discussion about alternatives or disagreement.
> Recapping the thread, the specific problem Steven raised was that
> catching ValueError will handle "float('not-a-valid-float')", but not
> "Decimal('not-a-valid-float')", as the latter raises
> decimal.InvalidOperation, which inherits from ArithmeticError rather
> than ValueError.
> There are 3 possible ways of changing that:
> - make ArithmeticError a subclass of ValueError
> - make decimal.InvalidOperation inherit from both ArithmeticError & ValueError
> - raise a subclass of InvalidOperation that also inherits from
> ValueError for decimal string conversions

FWIW, here is the exception hierarchy that I use in gmpy2. I've
ignored the standard
base classes, etc.

 - gmpyError
 - ArithmeticError
 - ValueError

- gmpyError
- ZeroDivisionError
- ArithmeticError

 - gmpyError
 - ArithmeticError

 - InexactResultError
 - gmpyError
 - ArithmeticError

 - InexactResultError
 - gmpyError
 - ArithmeticError

 - gmpyError
 - ArithmeticError

> The last one is clearly the lowest risk and lowest impact way to allow
> ValueError to be used to catch decimal string conversion problems, so
> in the absence of any other considerations, I'd just say "Let's do
> that".

Of the 3 options proposed by Nick, the first option would change
gmpy2.RangeError. The second option corresponds to the current approach
used by gmpy2.InvalidOperation. The third option introduces a new hierarchy.

> However, what came up in the course of the discussion is that those of
> us participating in the thread don't actually know the original
> rationale for ArithmeticError being entirely distinct from ValueError,
> rather than being a subclass of it, which means there are other cases
> where folks may be expecting ValueError to catch all conversion
> errors, but it may in fact be missing some. As a toy example:
>>>> def to_float(x):
> ...     try:
> ...         return float(x)
> ...     except ValueError:
> ...         return float("nan")
> ...
>>>> to_float("not-a-float")
> nan
>>>> to_float(10**1000)
> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "<stdin>", line 3, in to_float
> OverflowError: int too large to convert to float
>>>> from fractions import Fraction
>>>> class LazyFraction:
> ...     def __float__(self):
> ...         return float(Fraction(1, 0))
> ...
>>>> to_float(LazyFraction())
> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "<stdin>", line 3, in to_float
>   File "<stdin>", line 3, in __float__
>   File "/usr/lib64/python3.5/fractions.py", line 186, in __new__
>     raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
> ZeroDivisionError: Fraction(1, 0)
> Before this thread, I would have said that the above "to_float()"
> function would reliably return either the converted value or NaN in
> the absence of coding bugs in a __float__ method implementation, but
> the OverflowError and ZeroDivisionError cases above show that I would
> have been wrong about that, and a more comprehensive exception clause
> would be "except (ValueError, ArithmeticError):".
> If you rule out turning ArithmeticError into a ValueError subclass in
> 3.6+, then we can go back to looking specifically at the decimal
> string conversion behaviour. However, if you thought the builtin
> exception hierarchy change was an idea worth considering further, then
> it would address decimal string conversions as a side effect (since
> those are already an ArithmeticError subclass).

I've used the following guidelines for exceptions:

1) A TypeError is raised when there is no possible way to associate
     a meaning to an instance of that type.
2) A ValueError is raised when at least some instances of the given
     type can have a valid meaning but the particular value passed has no
     possible valid interpretation.
3) An ArithmeticError (or subclass) is raised when arithmetic operations
     on a valid input fail for a particular reason.

Some examples: int(float) raises a TypeError because there is no possible
way to assign a numeric value to a type. int(float("nan")) raises a ValueError
because there is no way to perform arithmetic operations on NaN.
int(float("inf")) raises OverflowError because while you can perform arithmetic
operation on Infinity, it will eventually overflow.

Following my guidelines, it makes sense that decimal.Decimal("not-valid")
would raise a (subclass of) ValueError.

> Regards,
> Nick.
> --
> Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/

More information about the Python-ideas mailing list