Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Deep interactions between Protocols and overloads #2058

Unanswered
RBerga06 asked this question in Q&A
Discussion options

Let's say I have a generic Protocol and a class that implements that protocol multiple times (for different arguments) via overloads:

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 Impl really does implement Proto the way I intended:

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 func that takes T and Proto[T], in any order, and just applies .proto(...) on them. Here, I'll model it as a generic class:

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 important

If I don't specify T manually, sometimes Pyright complains:

_ = 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 impl2: Impl[Two] to y: T, because Impl[Two] is not assignable to Impl[One]: it looks like Pyright has chosen the first overload in Impl's implementation of Proto and has resolved T to be One because impl1: Impl[One] indeed implements Proto[One]. However, since this leads to an error, why has Pyright not tried the other implementation?

Side note

If I don't manually enforce the type parameter of check1, Pyright infers Impl[One]:

reveal_type(check1(impl1)) # type is `check1[Impl[One]]` # first overload applies

Is this:

  • a hard limitation of Pyright (and type checker implementations in general),
  • a hole (unspecified behaviour) in the typing spec,
  • or the expected behaviour (and I just don't understand what's going on)?

If it is indeed a hard limitation, can I type func in a different way (without changing Impl and Proto)?

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,
Riccardo Bergamaschi.

You must be logged in to vote

Replies: 0 comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet
1 participant

AltStyle によって変換されたページ (->オリジナル) /