I noticed some severe divergences between different type checkers when calling a generic function that expects T | Sequence[T]
[1]. The problems arise when we feed in a type that is simultaneously a subtype of T
and of Sequence[T]
, indicating that the expected behavior is currently underspecificied.
# fmt: off
from collections.abc import Sequence
from typing import reveal_type as r
class Foo: ...
class Seq[F: Foo](Sequence[F], Foo): ...
def concat[L: Foo, R: Foo](
left: L | Sequence[L], # actually want `L | (Sequence[L] & ¬L)`
right: R | Sequence[R], # actually want `R | (Sequence[R] & ¬R)`
) -> list[L | R]:
return []
def test[F1: Foo, F2: Foo](
f1: F1, l1: list[F1], s1: Seq[F1],
f2: F2, l2: list[F2], s2: Seq[F2],
) -> None:
# results mypy pyright pyrefly ty
# foo + foo
r(concat(f1, f1)) # list[F1] list[F1] list[F1] e:arg-type
r(concat(f1, f2)) # list[F1 | F2] list[F1 | F2] list[F1 | F2] e:arg-type
# list + list
r(concat(l1, l1)) # list[F1] list[F1] list[F1] list[list[F1]]
r(concat(l1, l2)) # list[F1 | F2] list[F1 | F2] list[F1 | F2] list[list[F1] | list[F2]]
# seq + seq
r(concat(s1, s1)) # e:arg-type list[F1] list[F1] list[Seq[F1]]
r(concat(s1, s2)) # e:arg-type list[F1 | F2] list[F1 | F2] list[Seq[F1] | Seq[F2]]
# foo + list
r(concat(f1, l1)) # list[F1] list[F1] list[F1] e:arg-type
r(concat(f1, l2)) # list[F1 | F2] list[F1 | F2] list[F1 | F2] e:arg-type
# foo + seq
r(concat(f1, s1)) # e:arg-type list[F1] list[F1] e:arg-type
r(concat(f1, s2)) # e:arg-type list[F1 | F2] list[F1 | F2] e:arg-type
# list + seq
r(concat(l1, s1)) # e:arg-type list[F1] list[F1] list[Seq[F1] | list[F1]]
r(concat(l1, s2)) # e:arg-type list[F1 | F2] list[F1 | F2] list[Seq[F1] | list[F2]]
Take the case concat(Seq[F1], Seq[F1])
. So we first need to check and bind Seq[F1] <: L | Sequence[L]
and Seq[F1] <: R | Sequence[R]
.
- Since
Seq[F1] <: Foo
, we could bindL=Seq[F1]
. - Since
Seq[F1] <: Sequence[F1]
, we could also bindL=F1
.
So, depending on how the binding is done, one could expect one of several results:
list[F1]
,list[F1 | Seq[F1]]
,list[Seq[F1]]
Should a type checker pick one of these, and if so, which one and how/why? Or should all possible combinations be considered?
Playgrounds:
- mypy Playground
- Code sample in pyright playground
- Playground | ty
- pyrefly
actually, we want
T | (Sequence[T] & ¬T)
as in thestr
vsSequence[str]
issue, but type-checkers ideally should still come to the same conclusion regardless. ↩︎