Skip to content

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Jul 31, 2025

(This PR is easiest to review commit-by-commit. The first commit achieves all the improvements to semantics; following commits are focussed on improving performance.)

Summary

This PR removes the Tuple variant from our Type enum. Instead, Tuple types are now all represented as Type::NominalInstances with a certain TupleSpec stored as part of their generic specialisation.

Removing a Type variant leads to simpler code; the more Type variants we have, the more complex methods like Type::has_relation_to and Type::is_disjoint_from become. The main motivation for making this change is not to do with code complexity, however. Instead, it's to refocus how we think about tuple types.

Tuple types are obviously special in the Python type system in many ways. But one way in which they are exactly the same as all other NominalInstance type (and different to Literal types such as StringLiteral, BytesLiteral and BooleanLiteral) is that the type tuple[int, str] does not only include runtime objects where the __class__ is exactly tuple; it also includes instances of subclasses of tuple[int, str]. Explicit subclasses of specialised tuples are somewhat rare (though, as the ecosystem report on this PR shows, not as rare as you might think!) -- but tuple subclasses nonetheless appear an awful lot in Python code, since NamedTuples are popular, and every NamedTuple class is a tuple subclass. In order to achieve consistent and principled behaviour, all special-casing we apply to tuple[int, str] should ideally also be applied to subclasses of tuple[int, str]; but this is not something we've done up till now, and it's highly awkward to achieve this if instances of the tuple subclass are represented internally as Type::NominalInstances when the tuple supertype is represented internally as a Type::Tuple. Using Type::NominalInstance to represent both the tuple type and any subclasses of the tuple type means that it becomes easy to apply special casing to subclasses, and forces us to think explicitly about whether any special casing we add for tuples should apply to "just that tuple type" (though there's really no such thing, since the tuple type is a superset of all its subtypes) or to subtypes of that tuple type too.

Removing Type::Tuple therefore has the effect that all our tuple special casing is now applied to tuple subclasses as well as "exact tuple types":

  • We now infer precise types from slicing instances of tuple subclasses
  • Tuple subclasses can now be unpacked in the same way as tuples
  • Comparisons are now precisely inferred between instances of tuple subclasses in the same way as between instances of tuples. This allows us to consolidate our special casing for sys.version_info: rather than having dedicated branches in multiple places to handle sys.version_info comparisons, we now just treat sys.version_info as what it really is at runtime: an instance of a tuple subclass with a very particular tuple spec.
  • Tuple subclasses are now understood as sometimes being single-valued types in the same way as "exact tuple types".

Representing all tuple types using Type::NominalInstance also appears to have had the unexpected (but very welcome!) effect of improving our ability to solve TypeVars in several situations that showed up in the primer report (and for which I've added regression tests).

#19560, which is stacked on top of this, extends our special-casing for tuples and tuple subclasses to named tuples by inserting precise tuple types into the MRO of NamedTuple classes (currently, we model all NamedTuple classes as inheriting from tuple[Unknown, ...], which avoids false positives but at the cost of many false negatives).

Test Plan

Many mdtests added.

I haven't added any new tests specifically for precise comparison inference between instances of tuple subclasses, because this is already tested extensively (both explicitly and implicitly) by our support for sys.version_info branches. Our ability to infer sys.version_info >= (3, 9) as either True or False now just directly relies on us seeing that the sys._version_info class in typeshed is a tuple subclass (with a special-cased spec), and therefore taking the tuple-and-tuple-subclass path in TypeInferenceBuilder::infer_binary_type_comparison().

Ecosystem analysis

See #19669 (comment) for a full analysis. Overall, I think it looks great. The typing conformance diff also shows that this PR brings us closer to conformance with the typing spec.

Performance impact

Earlier versions of this PR caused large performance regressions, and also regressed memory usage. The latest version of this PR still causes some performance regressions, but they are all 1-2%, which is much reduced. I think partly the remaining regressions ared due to the fact that we're just doing a lot more work than we used to. We're able to infer precise types in many instances where we previously inferred Unknown, and we're able to solve many TypeVars that we previously couldn't.

I've experimented with adding Salsa caching to the methods on ClassType for figuring out what a class's tuple spec is. It appeared to speedup ty_micro[many_string_assignments] a little, but it had negligible difference on the other benchmarks (and possibly regressed performance slightly on some benchmarks, including colour-science).

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jul 31, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jul 31, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-08-11 20:53:33.906552447 +0000
+++ new-output.txt	2025-08-11 20:53:33.968552586 +0000
@@ -865,8 +865,6 @@
 tuples_type_compat.py:127:13: error[type-assertion-failure] Argument does not have asserted type `@Todo`
 tuples_type_compat.py:129:13: error[type-assertion-failure] Argument does not have asserted type `tuple[int | str, int]`
 tuples_type_compat.py:130:13: error[type-assertion-failure] Argument does not have asserted type `@Todo`
-tuples_type_compat.py:149:5: error[type-assertion-failure] Argument does not have asserted type `Sequence[int | float | complex | list[int]]`
-tuples_type_compat.py:152:5: error[type-assertion-failure] Argument does not have asserted type `Sequence[int | str]`
 tuples_type_compat.py:153:5: error[type-assertion-failure] Argument does not have asserted type `Sequence[Never]`
 tuples_type_compat.py:157:1: error[invalid-assignment] Object of type `tuple[Literal[1], Literal[""], Literal[""]]` is not assignable to `tuple[int, str]`
 tuples_type_compat.py:162:1: error[invalid-assignment] Object of type `tuple[Literal[1], Literal[1], Literal[""]]` is not assignable to `tuple[int, *tuple[str, ...]]`
@@ -886,4 +884,4 @@
 typeddicts_operations.py:60:1: error[type-assertion-failure] Argument does not have asserted type `str | None`
 typeddicts_type_consistency.py:101:1: error[invalid-assignment] Object of type `Unknown | None` is not assignable to `str`
 typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 887 diagnostics
+Found 885 diagnostics

@github-actions
Copy link
Contributor

github-actions bot commented Jul 31, 2025

mypy_primer results

Changes were detected when running on open source projects
parso (https://github.com/davidhalter/parso)
- parso/python/tokenize.py:230:42: error[invalid-argument-type] Argument is incorrect: Expected `tuple[str]`, found `set[Unknown]`
+ parso/python/tokenize.py:230:42: error[invalid-argument-type] Argument is incorrect: Expected `tuple[str]`, found `set[str]`

Expression (https://github.com/cognitedata/Expression)
- expression/extra/option/pipeline.py:91:19: error[invalid-argument-type] Argument to function `reduce` is incorrect: Expected `(def Some[_T1](value: _T1@Some) -> Option[_T1@Some], Unknown, /) -> def Some[_T1](value: _T1@Some) -> Option[_T1@Some]`, found `def reducer(acc: (Any, /) -> Option[Any], fn: (Any, /) -> Option[Any]) -> (Any, /) -> Option[Any]`
+ expression/extra/option/pipeline.py:91:19: error[invalid-argument-type] Argument to function `reduce` is incorrect: Expected `(def Some[_T1](value: _T1@Some) -> Option[_T1@Some], (Any, /) -> Option[Any], /) -> def Some[_T1](value: _T1@Some) -> Option[_T1@Some]`, found `def reducer(acc: (Any, /) -> Option[Any], fn: (Any, /) -> Option[Any]) -> (Any, /) -> Option[Any]`
- expression/extra/result/pipeline.py:96:19: error[invalid-argument-type] Argument to function `reduce` is incorrect: Expected `(def Ok[_TSource](value: _TSource@Ok) -> Result[_TSource@Ok, Any], Unknown, /) -> def Ok[_TSource](value: _TSource@Ok) -> Result[_TSource@Ok, Any]`, found `def reducer(acc: (Any, /) -> Result[Any, Any], fn: (Any, /) -> Result[Any, Any]) -> (Any, /) -> Result[Any, Any]`
+ expression/extra/result/pipeline.py:96:19: error[invalid-argument-type] Argument to function `reduce` is incorrect: Expected `(def Ok[_TSource](value: _TSource@Ok) -> Result[_TSource@Ok, Any], (Any, /) -> Result[Any, Any], /) -> def Ok[_TSource](value: _TSource@Ok) -> Result[_TSource@Ok, Any]`, found `def reducer(acc: (Any, /) -> Result[Any, Any], fn: (Any, /) -> Result[Any, Any]) -> (Any, /) -> Result[Any, Any]`

anyio (https://github.com/agronholm/anyio)
- src/anyio/_backends/_trio.py:909:47: error[not-iterable] Object of type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` may not be async-iterable
- Found 223 diagnostics
+ Found 222 diagnostics

starlette (https://github.com/encode/starlette)
- starlette/middleware/base.py:134:27: warning[possibly-unbound-attribute] Attribute `send` on type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is possibly unbound
- starlette/middleware/base.py:151:33: warning[possibly-unbound-attribute] Attribute `receive` on type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is possibly unbound
- starlette/middleware/base.py:154:37: warning[possibly-unbound-attribute] Attribute `receive` on type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is possibly unbound
- starlette/middleware/base.py:165:38: error[not-iterable] Object of type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` may not be async-iterable
- starlette/testclient.py:143:40: warning[possibly-unbound-attribute] Attribute `receive` on type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is possibly unbound
- starlette/testclient.py:143:60: warning[possibly-unbound-attribute] Attribute `send` on type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is possibly unbound
- starlette/testclient.py:691:52: error[invalid-argument-type] Argument is incorrect: Expected `ObjectSendStream[Unknown]`, found `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]`
- starlette/testclient.py:691:52: error[invalid-argument-type] Argument is incorrect: Expected `ObjectReceiveStream[Unknown]`, found `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]`
- starlette/testclient.py:692:55: error[invalid-argument-type] Argument is incorrect: Expected `ObjectSendStream[Unknown]`, found `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]`
- starlette/testclient.py:692:55: error[invalid-argument-type] Argument is incorrect: Expected `ObjectReceiveStream[Unknown]`, found `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]`
- tests/test_websockets.py:213:5: error[invalid-assignment] Object of type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is not assignable to `ObjectSendStream[MutableMapping[str, Any]]`
- tests/test_websockets.py:213:18: error[invalid-assignment] Object of type `MemoryObjectSendStream[Unknown] | MemoryObjectReceiveStream[Unknown]` is not assignable to `ObjectReceiveStream[MutableMapping[str, Any]]`
- Found 161 diagnostics
+ Found 149 diagnostics

dedupe (https://github.com/dedupeio/dedupe)
+ dedupe/predicates.py:116:20: error[invalid-return-type] Return type does not match returned value: expected `frozenset[Literal["0", "1"]]`, found `frozenset[str]`
+ dedupe/predicates.py:118:20: error[invalid-return-type] Return type does not match returned value: expected `frozenset[Literal["0", "1"]]`, found `frozenset[str]`
- Found 54 diagnostics
+ Found 56 diagnostics

trio (https://github.com/python-trio/trio)
- src/trio/_channel.py:546:38: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `MemoryReceiveChannel[Unknown]`, found `MemorySendChannel[T@as_safe_channel] | MemoryReceiveChannel[T@as_safe_channel]`
- src/trio/_core/_tests/test_guest_mode.py:524:66: error[invalid-argument-type] Argument to function `aio_pingpong` is incorrect: Expected `MemorySendChannel[int]`, found `MemorySendChannel[int] | MemoryReceiveChannel[int]`
- src/trio/_core/_tests/test_guest_mode.py:532:24: error[not-iterable] Object of type `MemorySendChannel[int] | MemoryReceiveChannel[int]` may not be async-iterable
- src/trio/_dtls.py:748:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `Unknown | MemorySendChannel[DTLSChannel] | MemoryReceiveChannel[DTLSChannel]` is possibly unbound
- src/trio/_dtls.py:789:29: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `Unknown | MemorySendChannel[bytes] | MemoryReceiveChannel[bytes]` is possibly unbound
- src/trio/_tests/test_channel.py:30:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:32:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:34:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:37:22: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:38:12: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:40:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:42:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:45:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:47:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:52:12: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:54:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:57:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:59:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | str | None] | MemoryReceiveChannel[int | str | None]` is possibly unbound
- src/trio/_tests/test_channel.py:66:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:68:11: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:86:41: error[not-iterable] Object of type `MemorySendChannel[int] | MemoryReceiveChannel[int]` may not be async-iterable
- src/trio/_tests/test_channel.py:108:23: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:132:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:134:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:138:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:140:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:151:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:153:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:168:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:170:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:190:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:192:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:196:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:198:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:209:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:211:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:226:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:228:15: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:237:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:249:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:255:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:266:19: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:269:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:276:22: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:287:19: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:290:22: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:297:15: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[str] | MemoryReceiveChannel[str]` is possibly unbound
- src/trio/_tests/test_channel.py:306:13: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:308:29: error[not-iterable] Object of type `MemorySendChannel[int] | MemoryReceiveChannel[int]` may not be async-iterable
- src/trio/_tests/test_channel.py:324:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:338:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:340:28: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:341:28: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:350:13: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:355:28: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[None] | MemoryReceiveChannel[None]` is possibly unbound
- src/trio/_tests/test_channel.py:366:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:367:12: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:368:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:369:12: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:383:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:385:13: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:392:5: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:394:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:396:28: warning[possibly-unbound-attribute] Attribute `send` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:398:16: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:400:13: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:401:23: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[int | None] | MemoryReceiveChannel[int | None]` is possibly unbound
- src/trio/_tests/test_channel.py:407:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:409:9: warning[possibly-unbound-attribute] Attribute `send_nowait` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:418:26: warning[possibly-unbound-attribute] Attribute `receive` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/test_channel.py:420:9: warning[possibly-unbound-attribute] Attribute `receive_nowait` on type `MemorySendChannel[int] | MemoryReceiveChannel[int]` is possibly unbound
- src/trio/_tests/type_tests/raisesgroup.py:30:5: error[invalid-assignment] Object of type `ExceptionGroup[Exception]` is not assignable to `ExceptionGroup[ValueError] | ValueError`
- src/trio/_tests/type_tests/raisesgroup.py:32:9: error[type-assertion-failure] Argument does not have asserted type `ExceptionGroup[ValueError]`
- src/trio/_tests/type_tests/raisesgroup.py:40:5: error[invalid-assignment] Object of type `BaseExceptionGroup[BaseException]` is not assignable to `BaseExceptionGroup[KeyboardInterrupt]`
- src/trio/_tests/type_tests/raisesgroup.py:154:5: error[invalid-assignment] Object of type `ExceptionGroup[Exception]` is not assignable to `ExceptionGroup[ExceptionGroup[ValueError]]`
- Found 728 diagnostics
+ Found 653 diagnostics

aiohttp-devtools (https://github.com/aio-libs/aiohttp-devtools)
+ tests/test_runserver_watch.py:108:5: error[invalid-assignment] Object of type `set[tuple[MagicMock, str]]` is not assignable to `set[tuple[WebSocketResponse, str]]`
- Found 123 diagnostics
+ Found 124 diagnostics

vision (https://github.com/pytorch/vision)
- torchvision/transforms/transforms.py:1779:16: error[unsupported-operator] Operator `<=` is not supported for types `tuple[float, float]` and `Literal[0]`, in comparing `(Unknown & Number) | (tuple[float, float] & Number)` with `Literal[0]`
+ torchvision/transforms/transforms.py:1779:16: error[unsupported-operator] Operator `<=` is not supported for types `tuple[float, float]` and `int`, in comparing `(Unknown & Number) | (tuple[float, float] & Number)` with `Literal[0]`

urllib3 (https://github.com/urllib3/urllib3)
- src/urllib3/filepost.py:72:9: warning[possibly-unbound-attribute] Attribute `write` on type `Unknown | tuple[bytes, int]` is possibly unbound
- src/urllib3/filepost.py:72:16: error[invalid-argument-type] Argument to bound method `__call__` is incorrect: Expected `str`, found `BytesIO`
- src/urllib3/filepost.py:79:13: warning[possibly-unbound-attribute] Attribute `write` on type `Unknown | tuple[bytes, int]` is possibly unbound
- src/urllib3/filepost.py:79:20: error[invalid-argument-type] Argument to bound method `__call__` is incorrect: Expected `str`, found `BytesIO`
- Found 396 diagnostics
+ Found 392 diagnostics

pywin32 (https://github.com/mhammond/pywin32)
- com/win32comext/shell/demos/create_link.py:65:13: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(str, Unknown, /) -> Unknown`, found `None`
+ com/win32comext/shell/demos/create_link.py:65:13: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `(str, Literal["SetPath", "SetArguments", "SetDescription", "SetWorkingDirectory"], /) -> Unknown`, found `None`

bokeh (https://github.com/bokeh/bokeh)
+ src/bokeh/layouts.py:666:16: error[invalid-return-type] Return type does not match returned value: expected `list[L@_parse_children_arg]`, found `list[L@_parse_children_arg | list[L@_parse_children_arg]]`
- src/bokeh/util/serialization.py:146:47: error[parameter-already-assigned] Multiple values provided for parameter `tzinfo` of function `__new__`

altair (https://github.com/vega/altair)
+ altair/utils/core.py:219:1: error[invalid-assignment] Object of type `frozenset[str]` is not assignable to `frozenset[Literal["field", "aggregate", "type", "timeUnit"]]`
- Found 1300 diagnostics
+ Found 1301 diagnostics

scikit-learn (https://github.com/scikit-learn/scikit-learn)
- sklearn/cluster/_hdbscan/hdbscan.py:131:8: error[unsupported-operator] Operator `>` is not supported for types `tuple[int, @Todo(Support for `typing.TypeAlias`)]` and `Literal[1]`
+ sklearn/cluster/_hdbscan/hdbscan.py:131:8: error[unsupported-operator] Operator `>` is not supported for types `tuple[int, @Todo(Support for `typing.TypeAlias`)]` and `int`, in comparing `tuple[int, @Todo(Support for `typing.TypeAlias`)]` with `Literal[1]`

zulip (https://github.com/zulip/zulip)
- zerver/lib/timestamp.py:21:42: error[parameter-already-assigned] Multiple values provided for parameter `tzinfo` of function `__new__`
- zerver/lib/timestamp.py:26:42: error[parameter-already-assigned] Multiple values provided for parameter `tzinfo` of function `__new__`
- Found 3729 diagnostics
+ Found 3727 diagnostics

sympy (https://github.com/sympy/sympy)
- sympy/matrices/expressions/slice.py:10:13: error[unsupported-operator] Operator `<` is not supported for types `tuple[Unknown, Unknown, Unknown]` and `Literal[0]`, in comparing `(Unknown & ~slice[Any, Any, Any] & ~tuple[Unknown, ...] & ~list[Unknown] & ~Tuple) | (tuple[Unknown, Unknown, Unknown] & ~tuple[Unknown, ...])` with `Literal[0]`
+ sympy/matrices/expressions/slice.py:10:13: error[unsupported-operator] Operator `<` is not supported for types `tuple[Unknown, Unknown, Unknown]` and `int`, in comparing `(Unknown & ~slice[Any, Any, Any] & ~tuple[Unknown, ...] & ~list[Unknown] & ~Tuple) | (tuple[Unknown, Unknown, Unknown] & ~tuple[Unknown, ...])` with `Literal[0]`
+ sympy/matrices/matrixbase.py:574:23: error[invalid-argument-type] Argument to function `reduce` is incorrect: Expected `(Self@hstack, Self@hstack, /) -> Self@hstack`, found `def row_join(self, other: Self@row_join) -> Self@row_join`
+ sympy/matrices/matrixbase.py:929:23: error[invalid-argument-type] Argument to function `reduce` is incorrect: Expected `(Self@vstack, Self@vstack, /) -> Self@vstack`, found `def col_join(self, other: Self@col_join, /) -> Self@col_join`
+ sympy/tensor/array/expressions/from_array_to_matrix.py:149:5: error[invalid-assignment] Object of type `list[Basic]` is not assignable to `list[Basic | None]`
- Found 12941 diagnostics
+ Found 12944 diagnostics

scipy (https://github.com/scipy/scipy)
+ scipy/integrate/tests/test_quadrature.py:124:31: error[invalid-argument-type] Argument to bound method `insert` is incorrect: Expected `int`, found `slice[Any, _StartT_co@slice, _StartT_co@slice | _StopT_co@slice]`
+ scipy/integrate/tests/test_quadrature.py:145:31: error[invalid-argument-type] Argument to bound method `insert` is incorrect: Expected `int`, found `slice[Any, _StartT_co@slice, _StartT_co@slice | _StopT_co@slice]`
- Found 6390 diagnostics
+ Found 6392 diagnostics
No memory usage changes detected ✅

@codspeed-hq
Copy link

codspeed-hq bot commented Jul 31, 2025

CodSpeed Instrumentation Performance Report

Merging #19669 will not alter performance

Comparing alex/remove-tuples (6797e9f) with main (2abd683)

Summary

✅ 42 untouched benchmarks

@codspeed-hq
Copy link

codspeed-hq bot commented Jul 31, 2025

CodSpeed WallTime Performance Report

Merging #19669 will not alter performance

Comparing alex/remove-tuples (6797e9f) with main (2abd683)

Summary

✅ 8 untouched benchmarks

@AlexWaygood AlexWaygood force-pushed the alex/remove-tuples branch 2 times, most recently from f3574de to 6def9b4 Compare July 31, 2025 20:01
@MichaReiser
Copy link
Member

What might give us more insight is if you could run ty with TY_MEMORY_REPORT=full on colour-science. Is there a significant increase of any salsa ingredient count (query or struct). If so, which one?

@AlexWaygood AlexWaygood force-pushed the alex/isinstance-truthiness-3 branch from 20cd78f to 83ea2df Compare August 1, 2025 13:27
Base automatically changed from alex/isinstance-truthiness-3 to main August 1, 2025 13:44
@AlexWaygood AlexWaygood force-pushed the alex/remove-tuples branch 4 times, most recently from 6da9eb6 to 08470a8 Compare August 1, 2025 18:41
@AlexWaygood AlexWaygood changed the base branch from main to alex/multi-valued-tuples August 1, 2025 18:41
@AlexWaygood AlexWaygood force-pushed the alex/multi-valued-tuples branch from 109d3e4 to 1e21124 Compare August 4, 2025 11:59
@AlexWaygood AlexWaygood force-pushed the alex/remove-tuples branch 2 times, most recently from 25627b3 to 85c8e81 Compare August 4, 2025 12:07
@AlexWaygood AlexWaygood force-pushed the alex/multi-valued-tuples branch from 1e21124 to ef1857e Compare August 4, 2025 13:12
@github-actions
Copy link
Contributor

github-actions bot commented Aug 4, 2025

ecosystem-analyzer results

Lint rule Added Removed Changed
possibly-unbound-attribute 0 73 0
invalid-argument-type 4 8 4
invalid-assignment 3 5 0
not-iterable 0 5 0
invalid-return-type 3 0 0
parameter-already-assigned 0 3 0
unsupported-operator 0 0 3
type-assertion-failure 0 1 0
Total 10 95 7

Full report with detailed diff

@AlexWaygood
Copy link
Member Author

AlexWaygood commented Aug 8, 2025

There's nothing standing out to me why this would regress performance so much other than that walking MROs might be expensive but the MRO should be cached already.

Have you tried capturing a perf profile locally and comparing it between main and this branch (it might be fruitless but just curious). I can try as well if that would be helpful

I believe #19811 (which is stacked on top of this PR) buys back almost all the performance and memory regressions from this change, so I think @carljm's analysis in #19669 (comment) was correct: this PR just meant that we were eagerly constructing a lot more unions and Specializations than we were previously; it's much better for performance to lazily figure out the union of all the element types in the union if and when we actually need to figure out the Sequence supertype of the tuple.

@AlexWaygood AlexWaygood changed the base branch from main to alex/cache-tuple-to-class-type August 8, 2025 22:22
Base automatically changed from alex/cache-tuple-to-class-type to main August 9, 2025 08:29
@AlexWaygood
Copy link
Member Author

I believe #19811 (which is stacked on top of this PR) buys back almost all the performance and memory regressions from this change, so I think @carljm's analysis in #19669 (comment) was correct: this PR just meant that we were eagerly constructing a lot more unions and Specializations than we were previously; it's much better for performance to lazily figure out the union of all the element types in the union if and when we actually need to figure out the Sequence supertype of the tuple.

I've merged the changes from #19811 into this one. This PR now passes the codspeed CI check: there are still regressions on many benchmarks, but they are nearly all 1-2% regressions (pydantic and sympy show regressions of 3%).

@AlexWaygood AlexWaygood force-pushed the alex/remove-tuples branch 3 times, most recently from 255f90a to 7db1dd2 Compare August 11, 2025 13:01
Comment on lines +133 to 176
Type::NominalInstance(instance) => match instance.class(db).known(db) {
// enum.nonmember
Some(KnownClass::Nonmember) => return None,

// enum.member
Some(KnownClass::Member) => Some(
ty.member(db, "value")
.place
.ignore_possibly_unbound()
.unwrap_or(Type::unknown()),
),

// enum.auto
Some(KnownClass::Auto) => {
auto_counter += 1;
Some(Type::IntLiteral(auto_counter))
}

_ => None,
},

_ => None,
};

if let Some(special_case) = special_case {
special_case
} else {
let dunder_get = ty
.member_lookup_with_policy(
db,
"__get__".into(),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;

match dunder_get {
Place::Unbound | Place::Type(Type::Dynamic(_), _) => ty,

Place::Type(_, _) => {
// Descriptors are not considered members.
return None;
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no semantic change here, I just micro-optimised this a bit to reduce the number of .class(db).is_known(db) calls,

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome!!


(Type::NominalInstance(left), Type::NominalInstance(right)) => left.class.cmp(&right.class),
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
left.class(db).cmp(&right.class(db))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this might still cause us to materialize classes for tuples in many cases where we don't really need to? We could pretty easily do something a bit more involved here to avoid that...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I wasn't sure about how far to go with that kind of optimisation generally. It felt like it made sense for methods already on NominalInstanceType, and it felt like it made sense to move commonly used methods like is_object() to NominalInstanceType (especially since for that one in particular, it also arguably improves ergonomics to move the method onto NominalInstanceType). But the premise underlying this PR is that we should basically be treating tuple-instance types the same as other NominalInstance types in 95% of cases -- adding new methods to NominalInstanceType that branch on the inner enum, purely for the purpose of optimisation, feels like it works against that principle to a certain extent?

TL;DR I don't plan to do this in this PR, but also wouldn't object to it being done if you feel like tackling it!

@AlexWaygood
Copy link
Member Author

Final Codspeed report shows 1% speedups on micro and macrobenchmarks! 😃 https://codspeed.io/astral-sh/ruff/branches/alex%2Fremove-tuples?runnerMode=Instrumentation

@AlexWaygood AlexWaygood merged commit d2fbf2a into main Aug 11, 2025
38 checks passed
@AlexWaygood AlexWaygood deleted the alex/remove-tuples branch August 11, 2025 21:03
dcreager added a commit that referenced this pull request Aug 12, 2025
* main:
  Don't cache files with diagnostics (#19869)
  [ty] support recursive type aliases (#19805)
  [ty] Remove unsafe `salsa::Update` implementations in `tuple.rs` (#19880)
  [ty] Function argument inlay hints (#19269)
  [ty] Remove Salsa interning for `TypedDictType` (#19879)
  Fix `lint.future-annotations` link (#19876)
  [ty] Reduce memory usage of `TupleSpec` and `TupleType` (#19872)
  [ty] Track heap usage of salsa structs (#19790)
  Update salsa to pull in tracked struct changes (#19843)
  [ty] simplify CycleDetector::visit signature (#19873)
  [ty] use interior mutability in type visitors (#19871)
  [ty] Fix tool name is None when no ty path is given in ty_benchmark (#19870)
  [ty] Remove `Type::Tuple` (#19669)
  [ty] Short circuit `ReachabilityConstraints::analyze_single` for dynamic types (#19867)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants