Skip to content

Pass return value on ValueError exceptions in the cmath/math modules #133895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
skirpichev opened this issue May 11, 2025 · 6 comments
Open

Pass return value on ValueError exceptions in the cmath/math modules #133895

skirpichev opened this issue May 11, 2025 · 6 comments
Labels
extension-modules C modules in the Modules dir type-feature A feature request or enhancement

Comments

@skirpichev
Copy link
Contributor

skirpichev commented May 11, 2025

Feature or enhancement

Proposal:

Currently the error handling happens this way for the cmath module (taken from module comments):

Each of the c_* functions computes and returns the C99 Annex G recommended result and also sets errno as follows: errno = 0 if no floating-point exception is associated with the result; errno = EDOM if C99 Annex G recommends raising divide-by-zero or invalid for this result; and errno = ERANGE where the overflow floating-point signal should be raised.

The ValueError raised for EDOM and the OverflowError - for ERANGE, but the Annex G result is hidden from the pure-Python world. Though, it might be helpful for applications. E.g. clog(-0+0i) returns -∞+πi and clog(+0+0i) returns -∞+0i - correct one-sided limits in the pole of log(). (BTW, something like PoleError could be better here, but that's another story.)

The mpmath and the gmpy2 (per default, if trap_divzero and/or trap_invalid context options aren't enabled) rather return special values per the C standard, not raise exceptions. And the mpmath also uses builtin float's and math/cmath functions for the fp context. Thus, to override current behavior of the stdlib - we need to catch ValueError from the called function and then process function arguments to return special values, i.e. essentially re-implement handling of special values. But they already are computed in cmath/math functions, so why not return this information with an exception? An example:

>>> import cmath
>>> try:
...     cmath.atanh(1)
... except ValueError as ex:
...     print(ex.value)
...     
(inf+0j)
Initial patch, working for most functions, not using math_error().
diff --git a/Modules/cmathmodule.c b/Modules/cmathmodule.c
index 81cbf0d554..d388241f94 100644
--- a/Modules/cmathmodule.c
+++ b/Modules/cmathmodule.c
@@ -36,6 +36,15 @@ class Py_complex_protected_return_converter(CReturnConverter):
         data.return_conversion.append("""
 if (errno == EDOM) {
     PyErr_SetString(PyExc_ValueError, "math domain error");
+
+    PyObject *exc = PyErr_GetRaisedException();
+    PyObject *value = PyComplex_FromCComplex(_return_value);
+
+    if (value) {
+        PyObject_SetAttrString(exc, "value", value);
+    }
+    Py_DECREF(value);
+    PyErr_SetRaisedException(exc);
     goto exit;
 }
 else if (errno == ERANGE) {

Similar happens in the math module and fix looks simple as well. I didn't check all cases, but it seems that most functions in the cmath/math modules actually compute correct (per C standard and Annex G) answers for special values.

Alternative approach: some global flag to turn on the gmpy2-like behavior (i.e. raise no exceptions, but return special values instead).

Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

No response

Linked PRs

@skirpichev skirpichev self-assigned this May 11, 2025
@skirpichev skirpichev added type-feature A feature request or enhancement extension-modules C modules in the Modules dir labels May 11, 2025
@skirpichev
Copy link
Contributor Author

CC @tim-one, @mdickinson

abhigyan631 added a commit to abhigyan631/cpython that referenced this issue May 12, 2025
@skirpichev
Copy link
Contributor Author

CC @serhiy-storchaka, does this make sense for you?

skirpichev added a commit to skirpichev/cpython that referenced this issue Jun 1, 2025
Now this information is available as the "value" attribute
of the ValueError exception object:

```pycon
>>> import cmath
>>> try:
...     cmath.log(complex(-0.0, 0))
... except ValueError as exc:
...     print(exc.value)
...
(-inf+3.141592653589793j)
```

Also uses the AC magic for return value of the cmath.rect().
@skirpichev
Copy link
Contributor Author

skirpichev commented Jun 1, 2025

Pr for cmath's part is available for review: #134995
Edit: #135008 - for the math module.

skirpichev added a commit to skirpichev/cpython that referenced this issue Jun 1, 2025
Now this information is available as the "value" attribute of the
ValueError exception object:

```pycon
>>> import math
>>> try:
...     math.gamma(-0.0)
... except ValueError as exc:
...     print(exc.value)
...
-inf
```
@skirpichev skirpichev removed their assignment Jun 1, 2025
@serhiy-storchaka
Copy link
Member

I don't know how useful this feature would be. On the one hand, it adds overhead in case an exception was raised. It is unfortunate because the EAFP principle is commonly used in Python. On other hand, if the user needs such information, there is no way to get it in other way. And the cost of raising and catching an exception is already high, so relative overhead is not so great. But I do not know if anyone really need it. This needs a wider discussion.

Should the value be added also for ZeroDivisionError (NaN or infinities) and OverflowError (plus/minus infinity for conversion from int or division)? For now, 0/2**2000 returns 0.0, but 0.0/2**2000 raises OverflowError.

@skirpichev
Copy link
Contributor Author

But I do not know if anyone really need it.

@serhiy-storchaka, my major motivation was the mpmath's fp context (fixed precision). It uses Python's floats and math/cmath functions.

But other mpmath contexts have (just like gmpy2) a different defaults: exceptions are not raised. Unfortunately, it's not easy to fix the fp context to match this behavior: essentially this require to re-implement handling of special values in all stdlib's functions. With proposed solution I could just wrap such functions with try-except block.

Should the value be added also for ZeroDivisionError (NaN or infinities) and OverflowError (plus/minus infinity for conversion from int or division)?

Yes, I think you are right, i.e.:

>>> gmpy2.mpfr(1)/gmpy2.mpfr(0)
mpfr('inf')

This needs a wider discussion.

Maybe.

Alternative solution might be some context notion for floating-point math, just like for the decimal module.

With this we can open door to more features of the platform's floating-point arithmetic, e.g. support different rounding modes. (This is a kinda possible with ctypes, but FE_DOWNWARD, FE_TONEAREST, FE_TOWARDZERO and FE_UPWARD values are system-specific.) And it's essentially for free.

What do you think?

@skirpichev
Copy link
Contributor Author

BTW, as I expected, computed values in exceptional cases (special values, overflows) might be incorrect in the cmath module. I found two examples in our test suite so far, fixed in #134995.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension-modules C modules in the Modules dir type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

2 participants