Closed qexat closed 1 month ago
Pyright is working as intended here, so I don't consider this a bug.
The typing spec doesn't dictate how and when type checkers should narrow types. Each type checker applies various rules and heuristics that attempt to balance type safety with usability. Your code sample demonstrates a case where both pyright and mypy have chosen to opt for usability over theoretical correctness.
From a type theory standpoint, your analysis is correct. The theoretically correct type would be tuple[Unknown, ...] | tuple[str, str]
which is equivalent to tuple[Unknown, ...]
. However, that's not what most developers would expect in this case, and it's probably not what they intend. Optimizing for theoretical correctness tends to produce many false positives in typical python code. Deviating from theoretical correctness improves usability but can produce false negatives, as you've shown. Type checkers try to strike a good balance between the two extremes.
You might be interested in these slides, which I presented at a Typing Meetup last year. It shows some of the practical tradeoffs that type checkers make when implementing narrowing for isinstance
checks.
Disclaimer-ish, I think this is a bug, but I'm not sure if it is a coincidence that MyPy also shares it with Pyright, or the consequence of something incorrect/under-specified in the Typing Spec.
Description
Given a variable
v
which type is the union of a protocolP
(e.g.SupportsIndex
) and a parametrized concrete typeC[T]
(e.g.tuple
,list
), on an instance checkisinstance(v, C)
,v
gets incorrectly narrowed toC[T]
inside the branch, which can lead to a contradiction - a more concrete explanation is following.Explanation
For the following explanation,
typing
is assumed to be imported.Let's define a function
foo
which has a parametervalue
which type is the union ofSupportsIndex
(the protocol) andtuple[str, str]
(the parametrized concrete type). Note that thetuple
type parameters do not have to be concrete themselves, they can also be protocols.Expected
Why? Let's define the following class:
An instance of
MySillyTuple
can be passed tofoo
(as it satisfiestyping.SupportsIndex
) and will also satisfyisinstance(value, tuple)
as it subclassestuple
. However, the contradiction with Pyright's current behavior lies in the fact that it is not atuple[str, str]
.The runtime obviously confirms this difference. If
typing.reveal_type
is replaced withprint
, thenfoo(MySillyTuple((3, 5))
will print(3, 5)
.VS Code extension or command-line Tested with Pylance-vendored Pyright (1.1.373) and with the CLI (1.1.379).
:warning: MyPy is also subject to this.