Include `math.sign`

I don’t know how to square this statement with the actual code in the math module:

This is the main wrapper code that calls a libm function where func will be a pointer to one of those functions. The condition isnan(r) && !isnan(x) does two things:

  • If the libm function returns a nan for non-nan input then diverge from libm’s behaviour and raise ValueError.
  • If the libm function returns a nan for nan input then match libm’s behaviour and allow the nan to pass through.

Same goes for the second condition isinf(r) && isfinite(x).

That looks like a very deliberate choice to allow nan pass through and there is a clear place in the code where a different choice could have been made: the check could just be if (isnan(r)) instead.

In any case this is a useful behaviour when it is the behaviour that you want and in that situation it is useful to have a set of functions that implements this consistently. There are also situations where it would be better for all nans in the input or output to result in an exception and in that situation it is useful to have a set of functions that implements that behaviour consistently. Ideally you want to be able to choose between these behaviours which would be possible if the math module had contexts.

What is strictly worse for everyone is having a set of functions that does not consistently implement either behaviour.

1 Like

One Switch to Rule Them All: csignum-fast v1.2.2 ⊙ Gold Edition is out since Jan 5, 2026.

csignum-fast is a high-performance, versatile CPython implementation of the universal sign function for Python.

Visit PyPI and Github (the Homepage link at the left side of PyPI) for details.

Properties:

  • Maximally optimized C++ code
  • Three additional keyword parameters
  • Safe, quick, thoroughly tested (121 tests in the test suite)

Feel free to contact me via my email.

1 Like

EDIT: see below

Maybe this is not obvious to everyone but I’m pretty sure that @acolesnicov is an AI bot commenting here, opening a PR in the CPython repo and uploading the csignum-fast package to PyPI. The code shown on GitHub is completely absurd for something as trivial as a signum function and I recommend that no one use it.

I suspect that this is related to the bots in this SymPy PR and this one in the safeyes repo and I am highly suspicious of the motives of whoever it is that controls these bots and what they plan to do (think e.g. xz-utils backdoor).

2 Likes

I agree with a lot of what you’re saying, and I think we have the same conclusion, but our reasoning is different.

100%

Yes.

I understand why people might want to think this way, but even if it’s a bold thing to say I don’t think that history matters.

I think what matters going forward is what’s best for math library in the way that it will be used by future users.

That said, I agree with what you’re pushing (nan-passthrough behavior) and that’s because it’s the most common behavior. That’s what users of float expect, IMO. And I don’t think we should make math any more complicated than it is.

If you want to raise on nans, you can use a more powerful math library like numpy. If you want performance with vectorized inputs, you should use a jitted math library like Jax.

For me, math is for small scripts where you’re operating on floats and don’t need vectorization or special behavior. In that case, the only behavior I would like to see is compatibility with the Array API so that if I ever take such a script and vectorize it, the behavior is preserved.

To that end, I think sign should return a float. This is consistent with the Array API. Moreover, look at how sign is used in practice. A lot of the time, it’s sign(x) * y * ... and therefore, you end up wanting the float anyway. This is why the Array API’s sign returns an array of the same type as its input (essentially int to int, float to float, complex to complex, and Boolean to Boolean).

And even if you make math.sign return an integer, you need to make cmath.sign return a complex number, which is too weird.

Deviation from the Array API has both this upgrade cost, but also a mental cost. I don’t want to have to remember different interfaces.

1 Like

@oscarbenjamin It was not obvious to me that acolesnicov is likely a bot or a strange actor, thanks for pointing it out. I’m busy but I have interest in trying to make a small contribution like this to python. If we get explicit support from a core dev, and if it’s possible for the implementation to be in python I would volunteer to work out how to make the PR. But if the implementation needs to be platform C I think it is too big a project for me right now since my familiarity with C is very low.


I would appreciate any high level administrative suggestion folks have for how to work through the float/nan-passthrough vs int/nan-exception behavior decision. I think a lot of the arguments have been laid out and a few folks have given their opinions. Maybe this is something where we need a core dev sponsor to just rule and make a decision on the information available? A vote doesn’t seem like a really good idea since this thread contains some probably non-typical cross section of python users and devs. Maybe a PEP could clarify the conversation by laying it out the whole argument in more detail but no one has clearly said that that is worth it.

So, if most of you are saying that math functions should maps nans to nans, what should one do if one would like to become aware of nans in the system, precisely because one doesn’t usually think about nans?

In my work floats are generally “allowed” to be numbers or plus/minus infinity. So those are the cases I think about. Sprinkling assert not math.isnan(x) clauses throughout the code would make things significantly less readable, so we don’t do that. But rarely a nan creeps in, because we’re using the legacy pandas library. Last time we had to notice results were weird, (which isn’t even guaranteed to be noticed,) then backtrack through the debugger, to eventually figure out where that nan was coming from. It would have been much smoother if the program had raised an Error, as soon as possible ideally.

I much prefer None over nan, because None forces you to deal with it explicitly. (Or at least None raises Errors much more easily, with operations like addition.)

I get that there are people who are ok with floats sometimes being a nan, and that the cost to those people of sign(nan) raising or returning None might be greater than the benefit it would bring me if sign(nan) does raise.

It needs to be in C because the whole math module is in C and it would more awkward to try to call Python code than just to include this. It is not much C code though:

diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c
index 11c46c987e1..c1ddc0369d7 100644
--- a/Modules/mathmodule.c
+++ b/Modules/mathmodule.c
@@ -1045,6 +1045,33 @@ FUNC1(fabs, fabs, 0,
       "fabs($module, x, /)\n--\n\n"
       "Return the absolute value of the float x.")
 
+/*[clinic input]
+math.sign -> double
+
+    x: double
+    /
+
+Return the sign of x.
+[clinic start generated code]*/
+
+static double
+math_sign_impl(PyObject *module, double x)
+/*[clinic end generated code: output=eee6df4a7fda8834 input=e686897cff180980]*/
+{
+    if (x > 0.0) {
+        return 1.0;
+    }
+    else if (x < 0.0) {
+        return -1.0;
+    }
+    else if (x == 0.0) {
+        return 0.0;
+    }
+    else {
+        return x; /* sign(nan) = nan */
+    }
+}
+
 /*[clinic input]
 math.floor
 
@@ -3088,6 +3115,7 @@ static PyMethodDef math_methods[] = {
     MATH_POW_METHODDEF
     MATH_RADIANS_METHODDEF
     {"remainder",       _PyCFunction_CAST(math_remainder), METH_FASTCALL,  math_remainder_doc},
+    MATH_SIGN_METHODDEF
     MATH_SIGNBIT_METHODDEF
     {"sin",             math_sin,       METH_O,         math_sin_doc},
     {"sinh",            math_sinh,      METH_O,         math_sinh_doc},

I’m sure someone (perhaps me) will be happy to make a PR if there is an agreement to add this. The rest of a PR is docs and tests.

1 Like

Seems a bit risky to allow the infinities if the nans are not allowed since then arithmetic can make nans.

I don’t think that having sign(nan) raise is really going to help your case much if all of the other functions in the math module don’t work like that. Conversely in a situation where you want nan pass through then you do need it to be implemented consistently by all the functions involved. Having sign be the only function that is different here would end up not being right for any use case.

Having discussed this with someone offline and looking into it more I have been persuaded that I was jumping to conclusions here. The timing of @acolesnicov’s arrival coincided with seeing actual AI bots in different forums but I was being too paranoid and then looking over the code in signum-fast set me off.

I am sorry @acolesnicov for accusing you of being a bot.

For portability, CPython does intend to enforce C standard requirements on platforms that screw up, and over the years many platforms have screwed up in many ways. Rather than deal with an ever-growing maze of #ifdef’s, the wrapper function you linked to tries to kill off the most common failures in a uniform way.

Read the comments before the linked code.

For the majority of one-argument functions these rules are enough
to ensure that Python’s functions behave as specified in ‘Annex F’
of the C99 standard, with the ‘invalid’ and ‘divide-by-zero’
floating-point exceptions mapping to Python’s ValueError and the
‘overflow’ floating-point exception mapping to OverflowError.

It’s a deliberate choice to make Python act as if the platform libm conformed to ‘Annex F’ of the C99 standard. Part of which is not complaining about a NaN result from a NaN input. We’re not at all trying to fight the C standards.

But by the time C got around to wrestling with IEEE-754 gimmicks, Python’s raising of OverflowError and ValueError in some exceptional cases was already established behavior, and for Python’s own backward compatibility was preserved by the wrapper.

There was no “one way to rule them all” - it was a sequence of tradeoffs.

5 Likes

Use numpy with np.seterr(invalid='raise') or Jax with jax_debug_nans.

This would break type annotations, which would force everyone to write if result is not None, which is a much higher cost for everyone.

Note a subtlety: the wrapper sets errno, but largely ignores what the libm function sets errno to. Because that’s the most common “failure mode”: inconsistences across platforms in how errno gets set. They’re generally quite good now, though. at returning infinites or NaNs when appropriate. So the wrapper looks at the classifications of the input and the result to deduce when things went wrong. Like: finite input and infinite result? “Overflow” to Python, regardless of whether libm set errno to ERANGE

No knowledge of the specific function being called is generally needed to make such deductions.

1 Like

It’s true for the mpmath, as it has definition for complex values as well, but not for the gmpy2:

>>> from gmpy2 import *
>>> sign(mpfr(1.1))
1
>>> type(_)
<class 'int'>

No :slight_smile:

The “bug” with Fraction (another instance: numerator/denominator attributes) is in fact - the bug with integer arithmetic in Python (true division returns a float). But the gmpy2 has a cure here:

>>> from gmpy2 import *
>>> mpz(1)/mpz(2)
mpfr('0.5')
>>> get_context().rational_division = True
>>> mpz(1)/mpz(2)
mpq(1,2)

That’s why I think that the context notion for floating-point arithmetic in Python is more important addition than discussed here “one-liner” function :wink:

No!

>>> from gmpy2 import *
>>> get_context().erange
False
>>> sign(nan())
0
>>> get_context().erange
True
2 Likes

So @tim.one where do you stand on adding a sign function?

There was a PR but it was closed because the associated issue was closed. The associated issue was closed citing that no core developers had agreed with the idea in this thread. Then some core developers came and said that they were interested in hearing opinions from you or Mark. It doesn’t look like we are going to hear from Mark though.

We have debated the various issues and I think it is clear that there are 3 options that some people like. The first two are what @jagerber laid out above:

  • math.sign(x) -> int (raise on nan).
  • math.sign(x) -> float (nan passes through).
  • Don’t add a math.sign function.

I would still prefer a float function even it does raise on nan but perhaps no one else agrees. Various people have already given their opinions on these and I don’t think there is more to say.

As far as I can tell this thread goes nowhere unless either:

  • You say that you support either of the first two options.
  • Someone writes a PEP.

It seems absurd to me to have a PEP for this but if that is where we are then at least it is good if everyone knows that that is where we are.

1 Like

A pep might be useful here for other reasons, but I would hope it isn’t needed for just this function.

The two things I could see a pep being useful to clear up are:

  1. A guiding rule for what functions belong in the math module going forward, and
  2. Finding a way to lay out a consistent definition for the behavior users should be able to expect from those functions around various behaviors involving nans, infinities, and non-nan domain errors, without having to consult each function separately. As much as it may seem reasonable to be able to disclaim behavior on a per-function basis, actually doing so is a recipe to having mistakes happen. I don’t consider the current bit on the documentation saying it matches the behavior of Annex F where appropriate as fulfilling this, especially given the number of functions that aren’t covered by it.

It’s specifically that second point, and the issues with trying to reconcile behavior modeling Annex F, while at the same time trying to provide a higher level abstraction, and python intentionally not differentiating between signaling and quiet nans that is what makes me somewhat averse to even adding this function.

Maybe this is something that really needed addressing a long time ago, with the functions that are really just there to have consistent behavior around libm-defined functions in their own submodule, and then anything else could have consistent rules that weren’t tied to that.

2 Likes

Lukewarm :wink: The debate here is too “head driven” for my tastes, not driven by demonstrated need. A “use case” isn’t just finding code that happens to use a feature, but also a user explaining why they’re doing it that way, and why various end cases are better off with one kind of treatment rather than another, and why alternatives are less attractive. From real life, not from abstract arguments. Such things can’t really be guessed at from staring at isolated code snippets.

The only invariant I’ve seen that carries weight is

x == abs(x) * sign(x)

But if x is a zero, that’s true regardless of what sign() returns (unless an infinity), and if x a NaN it’s never true.

So it’s no use in deciding about end cases. It’s certainly odd that no other implementation I’ve seen tries to preserve the sign of a zero, and I see no use for that either. The function is at heart trying to do the crudest partitioning, not the most fine-grained imaginable

EDIT: JavaScript’s math.sign does preserve the sign of 0.

$ node
Welcome to Node.js v24.11.1.
Type ".help" for more information.
> Math.sign(0)
0
> Math.sign(-0)
-0
> Math.sign(Math.nan)
NaN

It’s most troubling that MPFR returns an int, mapping NaN (like \pm 0) to 0 (yes, you can set trap_erange in gmpy2’s context to make it raise an exception in that case instead:

>>> import gmpy2
>>> nan = gmpy2.nan()
>>> gmpy2.sign(nan)
0
>>> gmpy2.get_context().trap_erange = True
>>> gmpy2.sign(nan)
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    gmpy2.sign(nan)
    ~~~~~~~~~~^^^^^
gmpy2.RangeError: sign() of invalid value (NaN)

).

Those people aren’t ill-informed. GMP attracts lots of the best in the world at this stuff. Why didn’t they return a float? I don’t know. I do know I respect their work, and never dismiss it off-hand. They may know things none of us thought of.

And they don’t support sign() at all for their complex type. Can’t say I see a use for it either, beyond some abstract notion of “because it’s possible” theoretical purity.

numpy alone makes a decent case for adding something exactly like it does. which means return a float, lose the sign of a zero, and let NaN pass through. But they do not, e.g., coerce other types to floats. Int in, int out. We shouldn’t either.

I posted before that I could live with that: a math.sign that accepted only floats. If, e.g. , math.integer wants its own sign(), that’s a different issue (albeit related).

None of which addressee\s a more basic question: is it suitable for inclusion in the core of the language? People doing industrial strength numeric work already have it in the extension modules they use, and few people in this topic have said "yes, I’d use that! - about time’. So it can’t really be sold on demand. I think it can be sold on the basis that it’s a simple function very widely supplied in other computing environments, from spreadsheets to Macsyma. Except in Perl :wink:

5 Likes

I dug up whatever was going on in my coding life that motivated me to make this thread. Here is the story. I’m not telling the story because I think it is a use case that puts the nail in the coffin the there should definitely be a math.sign function, but I’m telling it because I think it is a use case that satisfies Tim’s definition. I’ll also emphasize that the “need” is not great. As has been pointed out, this is a simple function that people can implement on their own. Was just thinking the implementation/maintenance burden would be low enough to justify adding a small function, maybe that is where I went wrong :slight_smile: .

Finally one more caveat: I was exploring this topic (and making this thread) around the time I was learning these finer details about how floats work and what nan means.

Ok, here’s the story. I developed a package call sciform for applying scientific formatting algorithms to numbers and number of uncertainty with a high degree of control over the formatting style. More control than is provided by the python built in format specification mini-language. However, like the python format specification mini-language, sciform has modes for controlling the sign character at the start of a number:

  • "-" where only negative numbers get a negative sign,
  • "+" where all numbers get either a - or + sign
  • " " where all numbers get either a blank space or + sign to give uniform string justification.

Around Feb 2024 it looks like I learned that Decimal and float representations of 0 and nan are signed and I wondered how sciform should handle that sign. Look at this diff in a PR I made:

Under the naive (specifically naive of the fact that 0 and nan are signed) original implementation I had, I think the behavior was that 0 and nan would always get a + sign. Indeed, I think this is how the python format specification mini-language behaves even if you use -0 or -nan.

Since sciform is interested in formatting numbers from physical experiments, I decided that it should NOT respect the idiosyncrasies of floating point number and also decided it doesn’t make sense to give 0 a sign. So under "+" formatting for 0 and nan we get " 0" and " nan" whereas any positive number would get a +. So 0 and nan get special-cased in that sciform doesn’t give them a sign but it does give them a space to preserve justification. This was my debatable preference and is not the point.

Bringing the discussion back to this current thread. I think I had two uses for a sign function in the python library at that time.

The first use case is that, as I was learning about these things I was probably trying to use a sign function to just much around with sign(-0) and sign(float("-nan")) to see what happens. Actually the functions we’re discussing would have failed helping me learn because none of the proposals here give sign(-0) == -1 or sign(float("-nan")) == -1. That is rather a job for copysign (which I discovered) or fsign which isn’t under discussion really.

But the second, and more important use case, is that if there was a sign function I think I may have refactored the get_sign_str function to use the sign function to get the sign of the user input number and then use a dictionary mapping the return value of sign to the corresponding sign string, e.g.

NEGATIVE_SIGN_STR_DICT = {+1: "", 0: "", -1: "-"}
POSITIVE_SIGN_STR_DICT = {+1: "+", 0: " ", -1: "-"}
SPACE_SIGN_STR_DICT = {+1: " ", 0: " ", -1: "-"}

Notably, even if sign passed nan through, I would still need to special case on nan input since nan can’t be used as a dictionary key in the way I want to use it here.

Of course I could spin my own sign function and do this refactoring anyways, but I think it is probably something like this that caused me to “reach for” math.sign and then become disappointed that it didn’t exist.

And then one final critical piece to the story: I am a physical scientist. In almost all of my work I have numpy so np.sign is handy. However, sciform is specifically meant to be a lightweight formatting package with zero third party dependencies. That was how I (1) found myself in a situation where I didn’t have numpy and (2) wanted a sign function.


Back to the “head driven” discussion. I am kind of curious for a “use case” satisfying Tim’s criteria for math.cos, or math.atan. In any application where I want to calculate the cosine of something I also have numpy. But, if there IS some application where someone is creating functions using math.cos or the other math functions then they could benefit from a math.sign function. Some physical models just have the sign function in them, and if someone is happening to study such a function in whatever context they’re using math.cos in, there is a case where they might want math.sign.

I think this example has been mentioned already, but maybe a student is doing is a pure-python physics simulation involving friction and they want to model the friction force

F = -\mu_K N sgn(v)

They’re in a context where they’re doing math using python but don’t have numpy because they’re a student.

If you don’t think this is a compelling enough use case for adding a python built-in sign function then I don’t think there’s going to be any way to convince you. I think this is pretty much as compelling as it gets for having a sign function since it’s so simple. I don’t think there’s some super extra compelling use case for sign out there beyond stuff like this made up example (I haven’t combed through the examples in peoples code search links yet).

The point is that there ARE use cases, even if they’re trivial like this one, but it’s a little tricky (as over 100 posts in this thread show) to get the edge cases right, so it would be nice if python just handed users a function where experts (from this thread) have already worked through that stuff for good.

2 Likes

Very different.

  • cos() and tan() are ubiquitous, not relative rarities.
  • If an environment doesn’t supply them, they’re beyond the abilities of most programmers to code reasonably well. Implementing sign() themselves is close to trivial, no matter what they want it to do.

Quit selling after you made the sale ::wink:. Already said

I think it can be sold on the basis that it’s a simple function very widely supplied in other computing environments, from spreadsheets to Macsyma.

As you said, you were surprised that it wasn’t here when you looked for it. That’s good enough :smile:. Not to nail the end cases, but to justify adding something suitable.

4 Likes

…or better a job for the signbit function that Python 3.15 has a gained. The problem with the proposed sign function is that there is no clear idea how to handle edge cases (return value is int or float, should nans be passed through, be reported as 0 or should the function raise on nan,…). If you do your own sign function, you do as you please. In any case, it is very easy to implement with the tools that are available.

The signbit function was added without much discussion since it is part of C already since C99 and Python’ s docs do refer to C and its Annex F for semantics of the math module. Certainly, it would be also easy to implement: def signbit(x): return copysign(1.0, x) < 0.0.

1 Like

Thanks Tim.

I think that a math.sign function that accepts only floats would just upset people who are used to being able to write e.g. math.cos(1) rather than math.cos(1.0). Having math.sign(1) raise an error is not the model that people would expect for a function in the math module.

NumPy’s function does coerce to other types i.e. specifically it turns float into np.float64. Otherwise though NumPy has many dtypes and there is a consistent model of mapping the input type to the same output type unless a promotion is needed (e.g. np.cos upgrades from integer types to floating point types). This is what NumPy users expect because all of NumPy works like that but it is not the model of the math and cmath modules.

Generally with sign what you are going to do is

x = np.sign(y) * z

where y and z are the same type. If np.sign always returned an integer type then the multiplication would have to convert back to float immediately. The same applies with the math module except with the understanding that when you use the math module you are always using the float dtype so the analogue is like:

x = math.sign(y) * z
X = np.sign(Y, dtype=float) * Z

where x, y and z are floats although it is allowed to pass an int for y. Likewise X, Y and Z are float arrays but it is allowed to pass an int array for Y. This is the same predictable behaviour as the relationship between e.g. math.pow and np.pow.

2 Likes