There are (too) many examples of “extreme overloading” in scipy-stubs
, for instance, the scipy.sparse._base._spbase.__mul__
signature looks like this:
@overload # Self[-Bool], /, other: scalar-like +Bool
def __mul__(self, /, other: bool | _ToBool) -> Self: ...
@overload # Self[-Int], /, other: scalar-like +Int
def __mul__(self: _SpFromIntT, /, other: onp.ToInt) -> _SpFromIntT: ...
@overload # Self[-Float], /, other: scalar-like +Float
def __mul__(self: _SpFromFloatT, /, other: onp.ToFloat) -> _SpFromFloatT: ...
@overload # Self[-Complex], /, other: scalar-like +Complex
def __mul__(self: _SpFromComplexT, /, other: onp.ToComplex) -> _SpFromComplexT: ...
@overload # sparray[-Bool], /, other: sparse +Bool
def __mul__(self: _SpArray, /, other: _spbase[_ToBool | _SCT_co]) -> _SpArrayOut[_SCT_co]: ...
@overload # sparray[-Bool], /, other: array-like +Bool
def __mul__(self: _SpArray, /, other: _To2DLike[bool, _ToBool]) -> coo_array[_SCT_co, _ShapeT_co]: ...
@overload # sparray[-Int], /, other: sparse +Int
def __mul__(self: _SpArray[_FromInt], /, other: _spbase[_ToInt8 | _SCT_co]) -> _SpArrayOut[_SCT_co]: ...
@overload # sparray[-Int], /, other: array-like +Int
def __mul__(self: _SpArray[_FromInt], /, other: _To2DLike[bool, _ToInt8]) -> coo_array[_SCT_co, _ShapeT_co]: ...
@overload # sparray[-Float], /, other: sparse +Float
def __mul__(self: _SpArray[_FromFloat], /, other: _spbase[_ToFloat32 | _SCT_co]) -> _SpArrayOut[_SCT_co]: ...
@overload # sparray[-Float], /, other: array-like +Float
def __mul__(self: _SpArray[_FromFloat], /, other: _To2DLike[int, _ToFloat32]) -> coo_array[_SCT_co, _ShapeT_co]: ...
@overload # sparray[-Complex], /, other: sparse +Complex
def __mul__(self: _SpArray[_FromComplex], /, other: _spbase[_ToComplex64 | _SCT_co]) -> _SpArrayOut[_SCT_co]: ...
@overload # sparray[-Complex], /, other: array-like +Complex
def __mul__(self: _SpArray[_FromComplex], /, other: _To2DLike[int, _ToComplex64]) -> coo_array[_SCT_co, _ShapeT_co]: ...
@overload # spmatrix, /, other: spmatrix
def __mul__(self: _SpMatrixT, /, other: _SpMatrixT) -> _SpMatrixT: ...
@overload # spmatrix[-Bool], /, other: sparse +Bool
def __mul__(self: spmatrix, /, other: _spbase[_ToBool]) -> _SpMatrixOut[_SCT_co]: ...
@overload # spmatrix[-Bool], /, other: array-like +Bool
def __mul__(self: spmatrix, /, other: _To2D[bool, _ToBool]) -> onp.Array2D[_SCT_co]: ...
@overload # spmatrix[-Int], /, other: sparse +Int
def __mul__(self: spmatrix[_FromInt], /, other: _spbase[_ToInt8]) -> _SpMatrixOut[_SCT_co]: ...
@overload # spmatrix[-Int], /, other: array-like +Int
def __mul__(self: spmatrix[_FromInt], /, other: _To2D[bool, _ToInt8]) -> onp.Array2D[_SCT_co]: ...
@overload # spmatrix[-Float], /, other: sparse +Float
def __mul__(self: spmatrix[_FromFloat], /, other: _spbase[_ToFloat32 | _SCT_co]) -> _SpMatrixOut[_SCT_co]: ...
@overload # spmatrix[-Float], /, other: array-like +Float
def __mul__(self: spmatrix[_FromFloat], /, other: _To2D[int, _ToFloat32]) -> onp.Array2D[_SCT_co]: ...
@overload # spmatrix[-Complex], /, other: sparse +Complex
def __mul__(self: spmatrix[_FromComplex], /, other: _spbase[_ToComplex64 | _SCT_co]) -> _SpMatrixOut[_SCT_co]: ...
@overload # spmatrix[-Complex], /, other: array-like +Complex
def __mul__(self: spmatrix[_FromComplex], /, other: _To2D[float, _ToComplex64]) -> onp.Array2D[_SCT_co]: ...
@overload # spmatrix[+Bool], /, other: scalar- or matrix-like ~Int
def __mul__(self: spmatrix[_ToBool], /, other: _SparseLike[opt.JustInt, Int]) -> spmatrix[Int]: ...
@overload # spmatrix[+Bool], /, other: array-like ~Int
def __mul__(self: spmatrix[_ToBool], /, other: _To2D[opt.JustInt, Int]) -> onp.Array2D[Int]: ...
@overload # spmatrix[+Int], /, other: scalar- or matrix-like ~Float
def __mul__(self: spmatrix[_ToInt], /, other: _SparseLike[opt.JustFloat, Float]) -> spmatrix[Float]: ...
@overload # spmatrix[+Int], /, other: array-like ~Float
def __mul__(self: spmatrix[_ToInt], /, other: _To2D[opt.JustFloat, Float]) -> onp.Array2D[Float]: ...
@overload # spmatrix[+Float], /, other: scalar- or matrix-like ~Complex
def __mul__(self: spmatrix[_ToFloat], /, other: _SparseLike[opt.JustComplex, Complex]) -> spmatrix[Complex]: ...
@overload # spmatrix[+Float], /, other: array-like ~Complex
def __mul__(self: spmatrix[_ToFloat], /, other: _To2D[opt.JustComplex, Complex]) -> onp.Array2D[Complex]: ...
@overload # Self[+Bool], /, other: -Int
def __mul__(self: _spbase[_ToBool], /, other: _FromIntT) -> _spbase[_FromIntT, _ShapeT_co]: ...
@overload # Self[+Int], /, other: -Float
def __mul__(self: _spbase[_ToInt], /, other: _FromFloatT) -> _spbase[_FromFloatT, _ShapeT_co]: ...
@overload # Self[+Float], /, other: -Complex
def __mul__(self: _spbase[_ToFloat], /, other: _FromComplexT) -> _spbase[_FromComplexT, _ShapeT_co]: ...
@overload # catch-all
def __mul__(self, /, other: _To2DLike[complex, Scalar] | _spbase) -> _spbase[Any, Any] | onp.Array[Any, Any]: ...
WIthout the comments this is practically impossible to work with. But even with them, it’s still extremely difficult to read.
But in this case, this proposed solution won’t help much, as there are only 2 parameters. The long-requested higher kinded type-parameters and intersection type would help, but I don’t think that having those features would help clean up your reset_index
examplle from pandas-stubs
.
An idea I’ve been playing with, that signifcantly help in both our cases, is something I can best describe as a “type-mapping”. It’s basically a generalization of a “typevar with constraints” such as AnyStr
, which is a type-mapping of str -> str
and bytes -> bytes
.
So for reset_index
, you could define a type-mapping that takes the inplace
and drop
types as input, and outputs the return type of reset_index
. It could, for example, be written with some flashy scala-esque syntax (that shouldn’t be taken too seriously) like
from typing import Literal as L
case type ResetIndexResult[InplaceT: L[True], DropT: bool, VT] = None
case type ResetIndexResult[InplaceT: L[False], DropT: L[True], VT] = Series[VT]
case type ResetIndexResult[InplaceT: L[False], DropT: L[False], VT] = DataFrame
def reset_index[InplaceT: bool = L[False], DropT: bool = L[True]](
self,
level: Sequence[Level] | Level | None = ...,
*,
drop: DropT = ...,
name: Level = ...,
inplace: InplaceT = ...,
allow_duplicates: bool = ...,
) -> ResetIndexResult[InplaceT, DropT, S1]: ...
The scipy-stubs
example has only 2 parameters, so I’m sure you can imagine how this type-mapping could also be applied there.
The advantage of this external definition, is that it’s reusable without depending on the specific parameter names or positions. In scipy-stubs
, this could at the very least result in a 50% LOC reduction. This is also the case for the stubs of numpy
, maybe even more so.