Adding parameters to math.comb and math.perm to accept signed inputs

I am an amateur mathematician from the OEIS, and use math.comb and math.perm frequently in my contributions there, during which it often behooves me to define

>>> C=lambda n, k: int(k>=0) and comb(n,k) if n>=0 else (-1)**(-n-k)*comb(~k,~n) if k<0 else (-1)**k*comb(k+~n,k)
>>> print(stratrix(tap(lambda n: tap(lambda k: C(n,k),range(-4,4)),range(-4,4))).replace(',',' '))
( 1         1 -4 10 -20 
 -3  1      1 -3  6 -10 
  3 -2  1   1 -2  3  -4 
 -1  1 -1 1 1 -1  1  -1 
            1           
            1  1        
            1  2  1     
            1  3  3   1)

for reasons that I will try to convince you of here!

  1. it continues each column and row backwards as a polynomial! (The one compromise is that the point (0,0) does not obey C(n,k) = C(n-1,k) + C(n-1,k-1), but this has to be broken to retain C(n,k) = C(n,n-k).)
  2. The gamma function has poles at the negative integers; if you change the factorials inside C(n,k) = n!/(k!*(n-k)!) with x!→Γ(x+1), then take the limit as you approach integer values of n,k (with n negative), Γ(n+1) is at a pole, and one of Γ(k+1) and Γ(n-k+1) (depending on k’s sign). Since the poles are simple (the residue of Γ(-n) is (-1)ⁿ/n!), one can use the opposite of L’Hîpital’s rule (replace poles with residues, rather than zeros with derivatives) to obtain the limit as n,k approach integers (in any pair of directions in their complex planes).
    You can rewrite the factorials as products, extend products to negative-cardinality ranges, and cancel the zeroes, for the same result. (See Extensions of the binomial by Peter Luschny (which gives an argument with a bidirectional matrix), this section of my page which is primarily about extending Stirling numbers.)
  3. (n choose k) counts the ways to create a k-set by drawing from an n-set. Via a stars and bars argument, one obtains that the function ((n multichoose k)) = (n+k-1 choose k) counts the ways to create a k-multiset by drawing with replacement.
    Increasing the minimum number of stars between each bar-pair is equivalent to making the interval shorter, so (defining for our convenience) (n part k) := (n-1 choose k-1) is the number of ways to partition the integer n into an ordered list of k positive integer parts. For these purposes, it is useful to have the rule (n choose -1) = int(n==-1) to yield (n part 0) = int(n==0), since we consider there to be one partition of 0 into 0 positive parts. (In general, when each part is >=c, the # of partitions is (n-1+(1-c)*k choose k-1), with ((k multichoose n)) being the c=0 case.) This point, I think, is the one most meaningful for people using Python.
  4. Continuing a column backwards as a polynomial is something combinatorialists have reason to do; a theorem from Richard P. Stanley’s PhD thesis states that the # of homomorphisms from a poset p to the totally ordered set [n] is a polynomial in n of degree |p| (denote that as ωₚ(n)), the number of unstrict homomorphisms (which loosen the condition to f(i)≀f(j)) is Ωₚ(n) = (-1)^|p| * ωₚ(-n). When p is itself the totally ordered set [k], ωₚ(n) = (n choose k) and `Ωₚ(n) = (-1)^k * (-n choose k) = ((n multichoose k)).

I would like to be able to write comb(n,k,cont=True) to enable the continuation outside the doubly-nonnegative quadrant.

For perm(n,k) = k! * comb(n,k) I would like the first factor to be permitted to be negative, since allowing the second to be would entail floats which aren’t so useful for discrete math; perhaps a fractions.perm function would be preferable?

2 Likes

I supported defining comb(n,k) for k < 0, like it is defined for k > n, but was outnumbered.

Extending it to n < 0 is more ambiguous. You will need to break one of nice properties. In your example it is C(n,k) = C(n-1,k) + C(n-1,k-1), but there are other possible variants.

2 Likes

I discussed the case of comb in the PEP thread for the math.integer module here and listed examples from different libraries/languages:

1 Like

CPython doesn’t intend to be “a pioneer” in numeric areas. That’s for specialized extensions, and in the core we fry to stick to things everyone agrees on. For comb(n, k), a case may be made for negative n, but not for negative k, because prior Python related art already does different things for negative k:

>>> from gmpy2 import comb
>>> comb(4, 2)
mpz(6)
>>> comb(-4, 2)
mpz(10)
>>> comb(-4, 3)
mpz(-20)
>>> comb(4, -2)
Traceback (most recent call last):
    ...
    comb(4, -2)
    ~~~~^^^^^^^
OverflowError: can't convert negative value to unsigned int
2 Likes

I do not think that difference between OverflowError and ValueError is significant.

comb(n, k) = comb(n, n-k). If we support k > n, it would be logical to support k < 0.

Not the point. The point is that gmpy2.comb() does not support negative k, period. That’s a fact, which “head arguments” can’t change :wink:

1 Like

Well, this is just a bug. This incompatibility wrt the CPython is not essential, better to get rid of it.

Though, Mathematica does, for instance:

Wolfram Language 14.3.0 Engine for Linux x86 (64-bit)
Copyright 1988-2025 Wolfram Research, Inc.

In[1]:= Binomial[4, -2]
Out[1]= 0

In[2]:= Binomial[4, 4- -2]
Out[2]= 0

Or mpmath:

$ python -m mpmath --no-ipython
>>> binomial(4, -2)
0.0
>>> binomial(4, 4 - -2)
0.0
>>> binomial(-4, -2)
0.0
>>> binomial(-4, 2)
10.0

I don’t think we should be biased by particular choice of the GNU GMP.

But there are several ways of extension for comb(): note the PascalBinomial() function along side with the Binomial().

I encourage OP to present a new documentation for the comb(). Right now this (and perm() too) has simple combinatorial meaning, but it not holds for negative arguments. We need to explain definition here, maybe also reasons for a particular choice, etc. Thinking about all this stuff — maybe it’s better suited for an external package.

1 Like

Neither do I. Can only repeat that core Python shouldn’t take stands on fringe behaviors when other specialized packages already do different things. with them. I’m biased neither for nor against what GMP does here, just that it’s evidence there is not universal agreement.

There is universal agreement about what comb() should do when both arguments are non-negative, and that’s what the core implements, and that’s what the core should stick to.

Yes, and “pick one” is not our area of competence.

Of which there are already several to pick from.

Which is more likely: that a typical Python programmer passing -2 as the second argument to comb() intended that, or that it was a logical error in their code? “Bad arguments” have value too, and especially for catching errors in code written by non-experts. It’s why sqrt(-1) raises an exception in Python, despite that the language supports complex numbers.

Those who want 1j can get it cmath.sqrt(), used by those who know what they’re doing.

But I expect the audience for negative arguments to comb() is far smaller than the one for complex numbers. I won’t give any time to it.

7 Likes

BTW, playing around with Wolfram Alpha on the web, its Binomial appears to accept arbitrary real and complex numbers too. presumably (but don’t know this) by rewriting as gamma function calls when an arg isn’t an int.

1 Like

This is not a problem at all. It would be a problem if they return different value for negative k.

If we raise error for k < 0, then we should also raise error for k > n, because this function is symmetric.

I prefer that math.comb and math.comb remain harmonized with itertools.combinations and itertools.permutations neither of which have any concept of negative n or negative k.

3 Likes

FTR, SciPy returns 0 for negative inputs: comb — SciPy v1.16.1 Manual

If N < 0, or k < 0, then 0 is returned.

As a mathematician myself, I would prefer that we do not change math.comb because it’s not the exactly the same as binom and neither is it the same as just using \Gamma. math.comb is really meant to represent the “[n]umber of ways to choose k items from n items without repetition and without order”. The binomial coefficient \binom{x}{y} coincides with the output of math.comb when x and y are nonnegative integers but conceptually, it can also be regarded as function of two variables defined through the \Gamma function. SciPy provides analytic continuation for that function (and makes the distinction with comb) but returns nan for negative x since we have a pole.

If we raise error for k < 0, then we should also raise error for k > n, because this function is symmetric.

For math.comb, I agree that we should actually allow comb(n, k) == 0 for k > n because there is no way to pick k items out n without repetition or order. But I don’t want to motivate this choice by the identity \binom{n}{k} = \binom{n}{n-k} as it is (strictly speaking) only well-defined for 0\leq k\leq n.


My take is: don’t change the meaning of math.comb. Its meaning is a combinatoric one, period. There is just no canonical definition of \binom{x}{y} for negative arguments unless we make a choice, but this is not a choice the standard library should be willing to make (at least to me). The article you linked with the Pascal, Riordan and Taylor conditions looks good in the sense that Maple and Mathematica made that choice (I guess this is based on generalization to negative integers n and compled-valued binomial coefficient).

7 Likes

another point that I forgot to mention in the initial proposal is that with this convention, for negative n we would have that sum(comb(n,k,cont=1)*x**k for k in range(M)) and sum(comb(n,n-k,cont=1)*x**k for k in range(M)) converge to (1+x)**n for |x|<1 and |x|>1 respectively. (This is tautological on its own, but becomes useful when doing things like getting an Mth-order approximation of the Laplace transform of a product of functions).

I prefer that math.comb and math.comb remain harmonized with itertools.combinations and itertools.permutations neither of which have any concept of negative n or negative k.

well, the negative-n math.comb(‘s absolute value) would be harmonized with itertools.combinations_with_replacement.

I agree with @picnixz:

Meaning comb(n, k) is the number of ways to choose k items from n items without repetition and without order.

That’s the definition Python always had in mind. If k \gt n there are 0 ways. If k \lt 0, the question doesn’t make sense.

I don’t think it’s an accident that many other generalizations use some spelling of binomial instead of comb.

3 Likes

I used to do combinatorics questions at mathematics competitions. And in that context, I sometimes found the interpretation where binomial(n, k) is a polynomial in n of degree k useful. So just because [comb’s] meaning is the combinatoric one, that doesn’t even necessarily mean that n needs to be an integer :wink:

But I can respect the standard-lib choosing to Error rather aggressively for functions like this.

1 Like

But you spelled it binomial, not comb. And that seems common when going beyond the combinatorial definition (where the inputs and outputs are cardinalities of finite sets, and so necessarily non-negative integers).

For example, under Maxima:

(%i0) binomial(n, 3);
(%o0) ((n-2)*(n-1)*n)/6

My replies would be different if the the suggestion were to add a new binomial function. As is, this is akin to asking that math.factorial accept floats too as a way to spell a shifted variant of math.gamma.

Such things don’t serve the vast bulk of users, to whom phrases like “analytic continuation” sound like AI hallucinations :wink:

The result of the combinatorial meaning of comb() is captured by a formula. The formula can be generalized in various ways that “make sense” in contexts beyond combinatorics, but doing so obscures the original - and intended, and simple - meaning.

Better to leave those to packages aimed at domain experts.

3 Likes

I agree with leaving comb alone.

1 Like