[ty] Recurse into tuples and nested tuples when applying special-cased validation of isinstance() and issubclass()#23607
Merged
AlexWaygood merged 7 commits intomainfrom Feb 27, 2026
Conversation
Previously, diagnostics for non-runtime-checkable protocols and issubclass checks against protocols with non-method members were only emitted when a single class literal was passed as the second argument. When a tuple of classes was passed (e.g. `isinstance(x, (P1, P2))`), the individual elements were not checked for protocol-related issues. Extract the class-literal checking logic into a recursive helper `check_classinfo_in_isinstance` that handles both single class literals and tuples (including nested tuples), checking each element for protocol and typed dict issues. https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
…vely Expand check_classinfo_in_isinstance to handle all validation that should apply recursively through tuple elements: - Protocol checks (non-runtime-checkable, non-method members) - TypedDict checks - typing.Any in isinstance() calls - Invalid elements in types.UnionType instances Previously, only protocol/TypedDict checks were recursive; the typing.Any and UnionType validations were only checked at the top level. Now all four validations work correctly when nested inside tuple arguments like isinstance(x, (int, Any)) or issubclass(y, (int, list[int] | str)). Also refactors the recursive protocols regression test to use @runtime_checkable instead of error annotations, so the test stays focused on what it's demonstrating (no stack overflow). https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
… chains Thread the classinfo AST expression through check_classinfo_in_isinstance so that when a UnionType is nested inside a tuple, the secondary annotation highlights just the UnionType expression rather than the entire tuple. Also use let chains for the isinstance truthiness check at the call site and the tuple recursion guard. https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
Make classinfo_expr optional so that check_classinfo_in_isinstance still validates all tuple elements even when the expression isn't a literal tuple in the AST (e.g., when the tuple is stored in a variable). When classinfo_expr is a literal tuple, we zip AST elements with type elements for precise secondary annotations. When it's not (or is None), we still perform all validation but skip the secondary annotation on UnionType diagnostics. Add tests for non-literal tuples across all validation types: protocols, TypedDict, typing.Any, and invalid UnionType elements. https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
…n is absent When an invalid UnionType is nested inside a non-literal tuple, we don't have an AST expression to use for the secondary annotation. In that case, include the union type in the info message so the user knows which union contains the invalid element. With annotation: "Element `list[int]` in the union is not a class object" Without annotation: "Element `list[int]` in the union `list[int] | bytes` is not a class object" https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
Add test coverage for nested tuples like `isinstance(x, (int, (str, P)))` across all validation types: protocols, TypedDict, typing.Any, and invalid UnionType elements. The secondary annotation correctly highlights just the problematic expression within the nested tuple. https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
Typing conformance results improved 🎉The percentage of diagnostics emitted that were expected errors increased from 85.37% to 85.39%. The percentage of expected errors that received a diagnostic increased from 75.37% to 75.46%. Summary
True positives addedDetails
|
|
Memory usage reportMemory usage unchanged ✅ |
|
charliermarsh
approved these changes
Feb 27, 2026
Member
charliermarsh
left a comment
There was a problem hiding this comment.
(Makes sense to me assuming my one comment is addressed.)
When `isinstance(*args)` is called, the AST only has one argument node (`*args`), but `parameter_types` resolves to two types after unpacking. The code was using `call_expression.arguments.args[1]` which panics with an index-out-of-bounds error. Fix by using `.get(1)` instead, which returns `None` when the second AST argument doesn't exist. The `check_classinfo_in_isinstance` function already handles `classinfo_expr: None` gracefully. https://claude.ai/code/session_01DRvdW91iMBPcFW5rjuZzab
carljm
added a commit
that referenced
this pull request
Feb 27, 2026
* main: [ty] Take myself out of the reviewer pool for the next few days (#23618) [ty] Fix bug where ty would think that a `Callable` with a variadic positional parameter could be a subtype of a `Callable` with a positional-or-keyword parameter (#23610) [`ruff`] Add fix for `none-not-at-end-of-union` (`RUF036`) (#22829) Bump cargo dist to 0.31 (#23614) [`pyflakes`] Fix false positive for names shadowing re-exports (`F811`) (#23356) [`fastapi`] Handle callable class dependencies with `__call__` method (`FAST003`) (#23553) [ty] Recurse into tuples and nested tuples when applying special-cased validation of `isinstance()` and `issubclass()` (#23607) Update typing conformance suite commit (#23606) [ty] Detect invalid uses of `@final` on non-methods (#23604) [ty] Move the type hierarchy request handlers to individual modules [ty] Wire up the type hierarchy implementation with the LSP [ty] Add routine for mapping from system path to vendored path [ty] Implement internal routines for providing the LSP "type hierarchy" feature [ty] Add some helper methods on `ClassLiteral` [ty] Move some module name helper routines to methods on `ModuleName` [ty] Bump version of `lsp-types` [ty] Refactor to support building constraint sets differently (#23600) [ty] Dataclass transform: neither frozen nor non-frozen (#23366) [ty] Add snapshot tests for advanced `invalid-assignment` scenarios (#23581) [ty] disallow negative narrowing on SubclassOf types (#23598)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refactor the validation logic for
isinstance()andissubclass()calls to support checking tuples (both literal and non-literal) that contain invalid types like protocol classes, TypedDicts,typing.Any, and invalidUnionTypeinstances.Previously, validation only worked when these invalid types were passed directly as the second argument. Now we recursively validate each element in tuples, enabling detection of errors in cases like:
isinstance(obj, (int, SomeProtocol))isinstance(obj, (int, SomeTypedDict))isinstance(obj, (int, typing.Any))isinstance(obj, (int, list[int] | bytes))This fixes astral-sh/ty#1600 and improves our typing conformance score
Test Plan
mdtests and snapshots extended and updated