# Support Function With Multiple Generic Parameters Same Type

**URL:** <https://discuss.python.org/t/support-function-with-multiple-generic-parameters-same-type/57931>\
**Category:** Typing\
**Tags:** typing\
**Created:** [July 11, 2024, 7:06pm UTC](https://discuss.python.org/t/support-function-with-multiple-generic-parameters-same-type/57931 "2024-07-11T19:06:00Z")\
**Posts on this page:** 1\
**Showing post:** 18

<div class="post-metadata">

**Author:** ![jorenham](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jorenham/32/27806_2.png) [@jorenham](https://discuss.python.org/u/jorenham)\
**Post date:** [November 6, 2026, 10:19pm UTC](https://discuss.python.org/t/support-function-with-multiple-generic-parameters-same-type/57931/18 "2026-11-06T22:19:12Z")

</div>

> [@sneakers-the-rat](#):
>
> yes, that is me that raised the issue!

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.

```py
# 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.

```py
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

```py
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](https://discuss.python.org/t/constrained-type-vars-with-any-default/104799))? 

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_.

---

_[View the full topic](https://discuss.python.org/t/support-function-with-multiple-generic-parameters-same-type/57931)._
