Skip to content

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

Description

@skirpichev

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

Activity

  1. self-assigned this
    on May 11, 2025
  2. skirpichev commented on May 11, 2025

    @skirpichev
    MemberAuthor
  3. added a commit that references this issue on May 12, 2025
  4. skirpichev commented on May 18, 2025

    @skirpichev
    MemberAuthor

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

  5. added a commit that references this issue on Jun 1, 2025
  6. skirpichev commented on Jun 1, 2025

    @skirpichev
    MemberAuthor

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

  7. added a commit that references this issue on Jun 1, 2025
  8. removed their assignment
    on Jun 1, 2025
  9. serhiy-storchaka commented on Jun 2, 2025

    @serhiy-storchaka
    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.

  10. skirpichev commented on Jun 3, 2025

    @skirpichev
    MemberAuthor

    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?

  11. skirpichev commented on Jun 5, 2025

    @skirpichev
    MemberAuthor

    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.

  12. added a commit that references this issue on Jun 17, 2025
  13. skirpichev commented on Aug 17, 2025

    @skirpichev
    MemberAuthor
  14. self-assigned this
    on Aug 17, 2025
  15. picnixz commented on Aug 17, 2025

    @picnixz
    Member

    I like the context notion as in C but this probably requires a PEP because it affects not just (c)math functions but the entire fp arithmetic. So for now, I wouldn't consider this solution though it's probably the cleanest.

    Raising ValueError with an attribute containing the special value would be more user friendly. Users indeed expect some exceptions if the arithmetic has issue and if we can add some canonical replacement as per ISO standards, that would probably be the best compromise.

    If we are worried because of some overhead in exceptions, how about a global flag for cmath and math functions that, if true, add the information. We should check if the overhead is really big or not, but I think people doing such computations only care about successful paths. I also thought that we had almost 0-cost exceptions for successful paths but was I wrong? (namely try-except are cheap if nothing bad occurs). When the exception occurs, I think the processing of this branch is likely to take more time than the processing of good inputs.

    So I am inclined in adding the information, whether conditionally or not. Alternatively we could create a ValueError subclass which holds the C value (not a PyObject) and transforms it on demand (only when calling .value for instance).

  16. skirpichev commented on Aug 18, 2025

    @skirpichev
    MemberAuthor

    I like the context notion as in C but this probably requires a PEP because it affects not just (c)math functions but the entire fp arithmetic.

    Yes, it does. Though, most of the complexity will be hidden per default. Unless user import the math module (I think it's a natural place to keep here context-related interfaces) and start changing the current context options.

    how about a global flag for cmath and math functions that, if true, add the information.

    That sounds as a bad version of context notion.

    I think people doing such computations only care about successful paths.

    Yes. I'm not sure that the EAFP principle is relevant here. Exception usually means the end of computation.

    we could create a ValueError subclass which holds the C value (not a PyObject) and transforms it on demand

    That will remove most of overhead, yes. And math/cmath modules deserve specialized version of a ValueError, which is too generic.

  17. skirpichev commented on Oct 15, 2025

    @skirpichev
    MemberAuthor
  18. skirpichev commented on Oct 19, 2025

    @skirpichev
    MemberAuthor

    Closing in favor of d.p.o thread.

  19. removed their assignment
    on Oct 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions