Support Function With Multiple Generic Parameters Same Type

Oh haha woops, sorry about that.

Completely understand that numpy would not offer these types directly, I am talking about downstream, stricter typing that is useful in specific contexts, even if it doesn’t make sense for an infrastructure-level library like numpy to use them directly.

Well, I think you misunderstood then, because NumPy also doesn’t support using Literal in shape-types in downstream libraries. Because in many cases (e.g. ndarray.T) it will give you incorrect results. You could still use it I guess, but that would then be at your own risk.

Anyway, I didn’t mean to derail this thread, so I’ll try to say something smart without mentioning Numpy now about the actual topic at hand in an attempt to get it back on track again:

Aside from the numpy examples, I still think the general principle of needing to be able to have typevars that don’t become implicitly widened unions stands alone, e.g. e.g. the “builtins.max can’t be typed correctly” as a motivating example

Yes I fully agree. I have wished for this to be possible myself many times, most of them while working on, erm, a certain numerical Python library (oof, close call).
Usually I was able to (kinda) work around it by either throwing some overloads at the problem, or by using a “TypeVar with constraints” (for the lack of a better term), e.g.

# IntegerT = TypeVar("IntegerT", bound=integer)  # won't work
EitherInt = TypeVar("EitherInt", uint32, uint64, int32, int64)  # subtypes of `integer`

# `mod(int32, uint64) -> float64`, so joining these as `int32 | uint64` would be wrong
def mod(a: EItherInt, b: EItherInt, /) -> EItherInt: ...

But clearly, that’s not ideal:

  • it’s usually not a general solution as it requires you to manually specify a finite set of types
  • subtypes of the constraints are upcast [1],
  • results in messy code [2],
  • the typing spec only partially defines how it should behave [3], and
  • several type-checkers (at least mypy and pyright) today have a bug where they eagerly select the first constraint when Any is passed [4].

Another workaround would be to use 4 overloads: one for each type. However, that will have many of the same problems, and when in reality you’re dealing with 8 integer dtypes + 4 floating dtypes + 3 complex dtypes + …, you’ll likely end up rage-quitting when heroically attempting to write out each overload and realize that there are two boolean parameters that affects the result type that you’ll have to overload too, which requires x4 the number of overloads…


Note how this would not be a problem if all variable inputs were in invariant positions, e.g.

IntegerT = TypeVar("IntegerT", bound=integer)

def mod_list(a: list[IntegerT], b: list[IntegerT]) -> list[IntegerT]: ...

Here, if you pass a: list[int32] and b: list[uint64], checkers will complain (as they should), because list[int32] and list[uint64] are not assignable to list[int32 | uint64], so there exists no joined type here that’s subject to bound=integer.

So what if there was an invariant type like list, say Just, so that for any T you can assign T to Just[T] and vice-versa? Then we could write the first mod as

def mod[T: integer](a: Just[T], b: Just[T], /) -> T: ...

and mod(int32(5), uint64(2)) will be rejected, as desired :).


  1. In my experience that’s usually not what you want, but in rare cases it can be handy. ↩︎

  2. especially when using PEP 695 syntax, have >2 constraints, and have to use this in several functions or overloads ↩︎

  3. What should happen when input is assignable to multiple constraints, e.g. when you pass Never? What if the input is a union of multiple constraints like int32 | int64? What about Any as a constraint (Constrained type vars with `Any` default)? ↩︎

  4. I call it a “bug” because not all materializations are considered, and this usually leads to unexpected and undesired outcomes, e.g. assigning Any to EitherInt will give you int8 (mumbles something about the gradual guarantee), But I guess you could also blame the typing spec here for not being clear on how to deal with this, technically speaking. ↩︎