[ty] Silence false-positive diagnostics when using typing.Dict or typing.Callable as the second argument to isinstance() (#21386)

This commit is contained in:
Alex Waygood 2025-11-11 19:30:01 +00:00 committed by GitHub
parent bd8812127d
commit 03bd0619e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 2 deletions

View file

@ -162,3 +162,38 @@ def _(x: A | B, y: list[int]):
reveal_type(x) # revealed: B & ~A reveal_type(x) # revealed: B & ~A
reveal_type(isinstance(x, B)) # revealed: Literal[True] reveal_type(isinstance(x, B)) # revealed: Literal[True]
``` ```
Certain special forms in the typing module are not instances of `type`, so are strictly-speaking
disallowed as the second argument to `isinstance()` according to typeshed's annotations. However, at
runtime they work fine as the second argument, and we implement that special case in ty:
```py
import typing as t
# no errors emitted for any of these:
isinstance("", t.Dict)
isinstance("", t.List)
isinstance("", t.Set)
isinstance("", t.FrozenSet)
isinstance("", t.Tuple)
isinstance("", t.ChainMap)
isinstance("", t.Counter)
isinstance("", t.Deque)
isinstance("", t.OrderedDict)
isinstance("", t.Callable)
isinstance("", t.Type)
isinstance("", t.Callable | t.Deque)
# `Any` is valid in `issubclass()` calls but not `isinstance()` calls
issubclass(list, t.Any)
issubclass(list, t.Any | t.Dict)
```
But for other special forms that are not permitted as the second argument, we still emit an error:
```py
isinstance("", t.TypeGuard) # error: [invalid-argument-type]
isinstance("", t.ClassVar) # error: [invalid-argument-type]
isinstance("", t.Final) # error: [invalid-argument-type]
isinstance("", t.Any) # error: [invalid-argument-type]
```

View file

@ -1028,6 +1028,13 @@ impl<'db> Type<'db> {
any_over_type(db, self, &|ty| matches!(ty, Type::TypeVar(_)), false) any_over_type(db, self, &|ty| matches!(ty, Type::TypeVar(_)), false)
} }
pub(crate) const fn as_special_form(self) -> Option<SpecialFormType> {
match self {
Type::SpecialForm(special_form) => Some(special_form),
_ => None,
}
}
pub(crate) const fn as_class_literal(self) -> Option<ClassLiteral<'db>> { pub(crate) const fn as_class_literal(self) -> Option<ClassLiteral<'db>> {
match self { match self {
Type::ClassLiteral(class_type) => Some(class_type), Type::ClassLiteral(class_type) => Some(class_type),

View file

@ -3647,6 +3647,31 @@ impl<'db> BindingError<'db> {
expected_ty, expected_ty,
provided_ty, provided_ty,
} => { } => {
// Certain special forms in the typing module are aliases for classes
// elsewhere in the standard library. These special forms are not instances of `type`,
// and you cannot use them in place of their aliased classes in *all* situations:
// for example, `dict()` succeeds at runtime, but `typing.Dict()` fails. However,
// they *can* all be used as the second argument to `isinstance` and `issubclass`.
// We model that specific aspect of their behaviour here.
//
// This is implemented as a special case in call-binding machinery because overriding
// typeshed's signatures for `isinstance()` and `issubclass()` would be complex and
// error-prone, due to the fact that they are annotated with recursive type aliases.
if parameter.index == 1
&& *argument_index == Some(1)
&& matches!(
callable_ty
.as_function_literal()
.and_then(|function| function.known(context.db())),
Some(KnownFunction::IsInstance | KnownFunction::IsSubclass)
)
&& provided_ty
.as_special_form()
.is_some_and(SpecialFormType::is_valid_isinstance_target)
{
return;
}
// TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
// here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
// silenced diagnostics during overload evaluation, and rely on the assignability // silenced diagnostics during overload evaluation, and rely on the assignability

View file

@ -1764,6 +1764,7 @@ impl KnownFunction {
Type::KnownInstance(KnownInstanceType::UnionType(_)) => { Type::KnownInstance(KnownInstanceType::UnionType(_)) => {
fn find_invalid_elements<'db>( fn find_invalid_elements<'db>(
db: &'db dyn Db, db: &'db dyn Db,
function: KnownFunction,
ty: Type<'db>, ty: Type<'db>,
invalid_elements: &mut Vec<Type<'db>>, invalid_elements: &mut Vec<Type<'db>>,
) { ) {
@ -1771,9 +1772,19 @@ impl KnownFunction {
Type::ClassLiteral(_) => {} Type::ClassLiteral(_) => {}
Type::NominalInstance(instance) Type::NominalInstance(instance)
if instance.has_known_class(db, KnownClass::NoneType) => {} if instance.has_known_class(db, KnownClass::NoneType) => {}
Type::SpecialForm(special_form)
if special_form.is_valid_isinstance_target() => {}
// `Any` can be used in `issubclass()` calls but not `isinstance()` calls
Type::SpecialForm(SpecialFormType::Any)
if function == KnownFunction::IsSubclass => {}
Type::KnownInstance(KnownInstanceType::UnionType(union)) => { Type::KnownInstance(KnownInstanceType::UnionType(union)) => {
for element in union.elements(db) { for element in union.elements(db) {
find_invalid_elements(db, *element, invalid_elements); find_invalid_elements(
db,
function,
*element,
invalid_elements,
);
} }
} }
_ => invalid_elements.push(ty), _ => invalid_elements.push(ty),
@ -1781,7 +1792,7 @@ impl KnownFunction {
} }
let mut invalid_elements = vec![]; let mut invalid_elements = vec![];
find_invalid_elements(db, *second_argument, &mut invalid_elements); find_invalid_elements(db, self, *second_argument, &mut invalid_elements);
let Some((first_invalid_element, other_invalid_elements)) = let Some((first_invalid_element, other_invalid_elements)) =
invalid_elements.split_first() invalid_elements.split_first()

View file

@ -328,6 +328,58 @@ impl SpecialFormType {
} }
} }
/// Return `true` if this special form is valid as the second argument
/// to `issubclass()` and `isinstance()` calls.
pub(super) const fn is_valid_isinstance_target(self) -> bool {
match self {
Self::Callable
| Self::ChainMap
| Self::Counter
| Self::DefaultDict
| Self::Deque
| Self::FrozenSet
| Self::Dict
| Self::List
| Self::OrderedDict
| Self::Set
| Self::Tuple
| Self::Type
| Self::Protocol
| Self::Generic => true,
Self::AlwaysFalsy
| Self::AlwaysTruthy
| Self::Annotated
| Self::Bottom
| Self::CallableTypeOf
| Self::ClassVar
| Self::Concatenate
| Self::Final
| Self::Intersection
| Self::Literal
| Self::LiteralString
| Self::Never
| Self::NoReturn
| Self::Not
| Self::ReadOnly
| Self::Required
| Self::TypeAlias
| Self::TypeGuard
| Self::NamedTuple
| Self::NotRequired
| Self::Optional
| Self::Top
| Self::TypeIs
| Self::TypedDict
| Self::TypingSelf
| Self::Union
| Self::Unknown
| Self::TypeOf
| Self::Any // can be used in `issubclass()` but not `isinstance()`.
| Self::Unpack => false,
}
}
/// Return the repr of the symbol at runtime /// Return the repr of the symbol at runtime
pub(super) const fn repr(self) -> &'static str { pub(super) const fn repr(self) -> &'static str {
match self { match self {