# The Idea

The idea is to add a `sign`

function to the `math`

module. The behavior for finite, non-zero floats/ints/decimals is obvious. Likewise for ± inf.

# The Utility

I think the utility of a `sign`

function is pretty obvious. It’s one of the most basic arithmetic operations. My main uses cases involve mathematically oriented code that needs to switch behavior based on the sign of some input.

# Current Workarounds

The current workarounds are

- Use some combination of
`x>0`

,`x>=0`

,`x<0`

,`x<=0`

, to check for the sign of`x`

. This may give surprising behavior if`x`

is ±nan or ±0. - One can hack a more thorough
`sign`

function using`math.copysign`

. Basically`sign(x) = copysign(1, x)`

. This option always returns ±1.0. For ±0 it returns ±1.0 and for ±nan it returns ±1.0. - If
`numpy`

is already a dependency one can use numpy.sign.`np.sign`

returns 0 for ±0 inputs and returns`nan`

for ±nan inputs. - There are likely others

# Edge cases

There are two edge cases. ±0 and ±nan.

Ideas for how to handle the edge cases:

- Always return the result of
`math.copysign(x, y)`

. With this form ±0 returns ±1.0 respectively and ±nan also returns ±1.0 (note that historically on some systems`math.copysign(1, float("nan"))`

would return`-1.0`

and vice-versa for`-nan`

, but on my system, Windows with Python 3.9 for the moment, the expected thing is returned). On this choice the`sign`

function would always return ±1.0. - Sometimes return
`0.0`

. For this choice`sign(float("0"))`

and`sign(float("-0"))`

would both return`0.0`

. Possibly the same would hold for ±nan. - Sometimes return
`nan`

. For this choice`sign(float("nan"))`

and`sign(float("-nan"))`

would both return`nan`

. Maybe the same would apply to`sign(0)`

- Sometimes return
`-nan`

for this case`sign(float("nan"))`

would return`nan`

but`sign(float("-nan"))`

would return`-nan`

.

The `np.sign`

behavior described above is a combination of the second and third behaviors. Note that `np.sign`

accepts complex input. For this **idea** I would first propose that `math.sign`

have the same behavior as `np.sign`

for all float (and decimal) inputs but that `math.sign`

not support complex input. However, I’m interested in discussions about alternative behaviors.

# Prior Discussion

It seems this was discussed around 2007 - 2009

The first google hit links to a stackoverflow question (2009). The main answer links to a rejected patch (2007) which included `math.sign`

and claims it was rejected because of lack of agreement on the edge cases citing this discussion (2008). The discussion specifically worries about handling of ±0.

Here is a issue (2009) showing that, for some systems and some versions of python `copysign`

gives surprising results for `nan`

. That is some combinations surprisingly gave

```
copysign(1.0, float("nan"))
# -1.0
copysign(1.0, float("-nan"))
# 1.0
```

While some expectedly gave

```
copysign(1.0, float("nan"))
# 1.0
copysign(1.0, float("-nan"))
# -1.0
```

On my current system and python version I get the expected behavior. I don’t know if that is because this behavior has been cleaned up in later versions of python or if it’s by luck on my system.

Here is a rejected issue (2016) requesting the `repr`

for `float("-nan")`

include a minus sign. The upshot of that discussion, citing stackoverflow answer (2014), was that “it is almost always a bug to attach meaning to the ‘sign bit’ of a NaN datum.” and, by this reasoning, `float("-nan")`

does the right thing by excluding the minus sign form the `repr`

.

# Response to the previous discussion

Agreement couldn’t be reached on edge cases back in the late 2000’s, but it looks like there wasn’t much discussion. I also don’t think any of the discussion was looking at the whole picture simultaneously in the way I’m trying to do here. That is, the discussions all seem to focus one specific edge case or another. Maybe agreement could be reached now. Also, `numpy`

was able to reach agreement, so maybe `python`

and `math`

can reach agreement now also.

Tangentially, I want to respond to the 2016 discussion about `repr(float("-nan"))`

not including the minus sign. I’ll point out that `repr(Decimal("-nan"))`

is `-NaN`

, including the decimal point. So there is an inconsistency here that could use resolution and help resolve confusion. The separate **idea** here is that neither or both should include the minus sign. I’d suggest what might be most consistent would be for neither to include the minus and `sign(float("nan"))`

and `sign(float("-nan"))`

both return `nan`

. This would strengthen the general stance taken by `python`

that the sign of `nan`

should be ignored. It will still be accessible through `copysign`

, but it could be documented there that significance shouldn’t be attached to the that sign. Note that some of the discussion above cite official references on floats saying things about the references being ambiguous about the significance of the sign of `nan`

, but I’m not familiar with those references.

To summarize a little bit: Yes, it’s weird that the `float`

specification supports ±0 and kind of allows ±nan. It doesn’t feel like that strange fact should preclude having a `math.sign`

function. It seems like a solvable problem. Rejecting `math.sign`

because of these edge cases feels like throwing the baby out with the bathwater.

# Who would maintain this?

I expect that `sign`

would be maintained by the current maintainers of `math`

. This “python experts” page indicates that that would be these people

```
mdickinson, rhettinger, stutzbach^
```

I have not contacted any of them directly about this suggestion yet. This is my first time suggesting this publicly.

A simple python implementation could look like

```
def sign(x, /):
if isnan(x):
return float("nan")
if x == 0:
return 0.0
return copysign(1, x)
```

Obviously typing bells and whistles etc. could be put on. Documentation would of course be required.

I hope the implementation is simple enough that the continuing maintenance burden would be low, but I don’t have personal experience with `stdlib`

maintenance. I would volunteer to help with the initial work and documentation.

# Closing

It seems a shame such a simple and expected feature was blocked by some non-critical edge cases. I fully expect the conclusion of this idea might just very quickly be: “This was discussed previously and rejected, that old decision stands”. Nonetheless, I’m curious about some renewed discussion.