The exact line of code I was looking at before starting this thread is line 660 here:
Specifically pyright objects to _dup(f) although mypy does not (I think mypy implicitly uses Any in dmp). The dmp[T] type is a list-of-lists-of-…-of-T explained in a previous thread and the MPZ type in another.
Actually I had misremembered which types are generic. The constrained type variable approach can be used for this particular function because int and MPZ are both concrete types (unusual in this code).
There are definitely other cases where a constrained type variable does not work though because it is more like this which is just my second example from above but with E being a type variable rather than a class:
from typing import Protocol
class F:
pass
class Domain[E: F](Protocol):
def convert(self, x: E | int) -> E:
...
def convert_list[E: F](x: list[E] | list[int], K: Domain[E]) -> list[E]:
return [K.convert(xi) for xi in x]
def set2list[T](x: set[T]) -> list[T]:
return list(x)
def convert_set[E: F](x: set[E] | set[int], K: Domain[E]) -> list[E]:
y = set2list(x) # <-- complains here
return convert_list(y, K)
I think my original question in this thread was answered by Jelle above. Yes, it could be possible in principle for type checkers to distribute a union of matching types over a generically typed function although no type checkers currently implement that. If they did then this code would check fine without modification.
There are many ways to work around that though. Perhaps since convert_set and dmp_normal are intended to convert an input that is not yet in the valid form for the type then maybe in this one place the annotation could change from set to covariant Set (or Iterable etc) as suggested above. Everywhere else the types like list[E] etc are supposed to be the exact stated types and e.g. Sequence is not appropriate but maybe here it is okay.
Any suggestions about the annotations in the code shown are certainly welcome though. This is the first part of SymPy to have relatively complete type annotations and it has taken a while to figure out how to write them. The main weakness I see is the recursively defined dmp type:
_T = TypeVar("_T")
dup: TypeAlias = "list[_T]"
dmp: TypeAlias = "list[dmp[_T]]"
It seems like pyright understands this type but I’m not convinced that mypy does:
from typing import reveal_type
type dmp[T] = list[dmp[T]]
def dmp_func[T,S](f: dmp[T], g: dmp[S]):
reveal_type(f)
reveal_type(f[0])
reveal_type(f[0][0])
f = g # error
$ mypy t.py
t.py:6: note: Revealed type is "builtins.list[...]"
t.py:7: note: Revealed type is "builtins.list[...]"
t.py:8: note: Revealed type is "builtins.list[...]"
Success: no issues found in 1 source file
$ pyright t.py
t.py:6:17 - information: Type of "f" is "list[dmp]"
t.py:7:17 - information: Type of "f[0]" is "dmp[T@dmp]"
t.py:8:17 - information: Type of "f[0][0]" is "dmp[T@dmp]"
t.py:9:9 - error: Type "dmp[S@dmp_func]" is not assignable to declared type "dmp[T@dmp_func]"
"builtins.list" is not assignable to "builtins.list"
Type parameter "_T@list" is invariant, but "dmp" is not the same as "dmp"