Deep interactions between Protocols and overloads
#2058
-
|
Let's say I have a generic class Proto[T](Protocol):
@type_check_only
def proto(self, _: T, /) -> None: ...
type One = Literal[1]
type Two = Literal[2]
@final
class Impl[N: int]:
# ensure `N` is invariant
def __init__(self, n: N, /) -> None: ...
@type_check_only
def get_n(self, /) -> N: ...
# implement Proto[N] for Impl[N] and Proto[M] for Impl[One] (for all integers N, M)
@overload
def proto(self, _: "Impl[N]", /) -> None: ...
@overload
def proto[M: int](self: "Impl[One]", _: "Impl[M]", /) -> None: ...I can easily verify that class check1[T]:
def __init__(self, x: Proto[T], /) -> None: ...
# Here it all works as expected
impl1 = Impl[One](1)
impl2 = Impl[Two](2)
impl_ = Impl[int](3)
_ = check1[Impl[One]](impl1) # ok (first overload applies)
_ = check1[Impl[Two]](impl1) # ok (second overload applies)
_ = check1[Impl[One]](impl2) # error (second overload doesn't apply because One != Two)
_ = check1[Impl[Two]](impl2) # ok (first overload applies)
_ = check1[Impl[One]](impl_) # error (second overload doesn't apply because N is invariant)
_ = check1[Impl[Two]](impl_) # error (second overload doesn't apply because N is invariant)
_ = check1[Impl[int]](impl_) # ok (first overload applies)Now, let's say we wanted to write a function class func[T]:
@overload
def __init__(self, x: T, y: Proto[T], /) -> None: ...
@overload
def __init__(self, x: Proto[T], y: T, /) -> None: ...
# implementation not importantIf I don't specify _ = func[Impl[One]](impl2, impl1) # error (correct)
_ = func[Impl[One]](impl1, impl2) # error (correct)
_ = func[Impl[int]](impl_, impl1) # ok
_ = func[Impl[int]](impl1, impl_) # ok
_ = func[Impl[Two]](impl2, impl1) # ok
_ = func[Impl[Two]](impl1, impl2) # ok
reveal_type(func(impl2, impl1)) # ok, type is `func[Impl[Two]]`
reveal_type(func(impl1, impl2)) # error, but why?Code sample in pyright playground The error message says I cannot assign Side noteIf I don't manually enforce the type parameter of reveal_type(check1(impl1)) # type is `check1[Impl[One]]` # first overload appliesIs this:
If it is indeed a hard limitation, can I type Thank you for your time and dedication to improve the soundness and expressiveness of the Python type system (and to answer all these questions from the community). Best regards, |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
|
Hi! What you’re running into here isn’t a special “typing bug” in your code, it’s a consequence of how overload resolution and structural subtyping interact in current Python type checkers. The key points are:
So in short:
|
Beta Was this translation helpful? Give feedback.
Hi! What you’re running into here isn’t a special “typing bug” in your code, it’s a consequence of how overload resolution and structural subtyping interact in current Python type checkers.
The key points are:
Static type checkers like Pyright and mypy match overloads based on the call signature alone. They don’t re-solve type variables based on deeper protocol conformance after the fact — once a candidate overload is chosen, they stick with it. This is why in your second call example Pyright picks the first overload branch and doesn’t try the other: there isn’t a mechanism in the spec that says “if this overload leads to a constraint contradiction, try another” — overload resolution is…