Skip to content

Inconsistency: NaN ** 0.0 = 1.0, but NaN * 0.0 = NaN #133274

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

Closed
BikeCrusader opened this issue May 1, 2025 · 3 comments
Closed

Inconsistency: NaN ** 0.0 = 1.0, but NaN * 0.0 = NaN #133274

BikeCrusader opened this issue May 1, 2025 · 3 comments
Assignees
Labels
type-bug An unexpected behavior, bug, or error

Comments

@BikeCrusader
Copy link

BikeCrusader commented May 1, 2025

Bug report

Bug description:

x = float("NaN")
# Prints "1.0"
print(x**0.0)
# Prints "nan"
print(x*0.0)
# Prints "nan"
print(x/float("inf"))
# Prints "nan"
print(x+float("inf"))
# Prints "nan"
print(x-float("inf"))

As you can see, the behavior of ** is inconsistent with that of * and +. ** treats NaN like a normal number,
so NaN ** 0.0 = 1.0. But other operations like * and + treat NaN like a special value, since it is not a number.
Therefore, the result of operations like NaN * 0.0 are NaN.
"""

CPython versions tested on:

3.13, 3.11

Operating systems tested on:

Windows

@BikeCrusader BikeCrusader added the type-bug An unexpected behavior, bug, or error label May 1, 2025
@StanFromIreland
Copy link
Contributor

StanFromIreland commented May 1, 2025

Can someone reformat the post to use codeblocks please.

cc @skirpichev

edit: It's not a bug, I read the messy post wrong, this should be closed (It is well documented :-)

The 2008 version of the IEEE 754 standard says that pow(1, qNaN) and pow(qNaN, 0) should both return 1 since they return 1 whatever else is used instead of quiet NaN.

https://en.wikipedia.org/wiki/NaN

https://docs.python.org/3/library/math.html#math.pow

@tim-one
Copy link
Member

tim-one commented May 1, 2025

This won't be changed - it's required behavior by various standards. And it's not alone:

>>> math.hypot(math.inf, math.nan)
inf
>>> math.hypot(math.nan, math.inf)
inf
>>> pow(math.nan, 0.)
1.0
>>> pow(1., math.nan)
1.0

The rationale for these exceptions is that some functions in some portion of their domain are independent of one or more of their operands. Thus, for example, hypot(x,+/-inf) is independent of x. When this is the case, such a function may be written so as not to even READ the other operand as it is not needed to know the result.

That's from a public post by an IEEE committee member here. You're not required to like it, but, as that post says,

These two camps were irreconcilable. And the situation stood with both groups of functions for some years.

One side eventually "won", and the "always propagate NaNs" side lost. They already debated it beyond death, and won't revisit it.

In brief, pow(1, y) returns 1 for any non-NaN y. So it doesn't matter that you don't know what y is - the result is 1 regardless. That's very different from, e.g., 1 * y, which very much depends on the precise value of y.

I also happen to agree with the post's close:

It was a judgement call. I think it was a good one. But you may differ in that opinion.

@tim-one tim-one closed this as not planned Won't fix, can't repro, duplicate, stale May 1, 2025
@skirpichev
Copy link
Member

skirpichev commented May 2, 2025

It is well documented :-)

It's is documented, indeed. But, maybe, not well. Here is my hubly attempt to read it.

Section 6.2 says:

The power operator has the same semantics as the built-in pow() function, when called with two arguments: it yields its left argument raised to the power of its right argument. The numeric arguments are first converted to a common type, and the result is of that type.

We also have rules for conversion:

When a description of an arithmetic operator below uses the phrase “the numeric arguments are converted to a common real type”, this means that the operator implementation for built-in types works as follows: [...] if either argument is a complex or a floating-point number, the other is converted to a floating-point number; [...]

So, we reasonable could conclude, that float1**float2 is an equivalent of pow(float1, float2). Unfortunately, the pow() builtin doesn't describe behavior for special arguments, it even doesn't mention the math.pow() as own equivalent with floating-point arguments.

In turn, the math.pow() docs indeed says something relevant:

Exceptional cases follow the IEEE 754 standard as far as possible. In particular, pow(1.0, x) and pow(x, 0.0) always return 1.0, even when x is a zero or a NaN.

Also, difference wrt builtin pow() is noted:

Unlike the built-in ** operator, math.pow() converts both its arguments to type float.

Edit: and pow() and math.pow() aren't actually equal wrt exception handling or complex results:

>>> import math
>>> pow(0.0, -1.0)
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    pow(0.0, -1.0)
    ~~~^^^^^^^^^^^
ZeroDivisionError: 0.0 cannot be raised to a negative power
>>> math.pow(0.0, -1.0)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    math.pow(0.0, -1.0)
    ~~~~~~~~^^^^^^^^^^^
ValueError: math domain error
>>> pow(-1.0, 0.5)
(6.123233995736766e-17+1j)
>>> math.pow(-1.0, 0.5)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    math.pow(-1.0, 0.5)
    ~~~~~~~~^^^^^^^^^^^
ValueError: math domain error

IMO, it might be not something obvious for newbie to search details of pow() on the math module page. I think there is a documentation issue.

Looking more in code, I found that float_pow() (nb_power slot implementation for float's) and math.pow() are actually two independent wrappers to the libm's pow(). I took some time from me to realize that (as was expected) they match indeed. If those workarounds are still relevant for nowadays libm - I think it's better to put them in one place somewhere (Module/_math.h? Edit: yes, required - for MacOS and WASI builds, see skirpichev#8). BTW, math.pow existence seems to be more a historical quirk for me: math.pow(x, y) == pow(float(x), float(y)). On another hand, there is no cmath.pow() (there is cpow() in libm!). (This function will be less trivial.)

@skirpichev skirpichev self-assigned this May 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants