Skip to content

[ty] Promote Literal types when inferring elements for very large unannotated tuples#22841

Merged
AlexWaygood merged 1 commit intomainfrom
alex/big-slow-tuples-2
Jan 27, 2026
Merged

[ty] Promote Literal types when inferring elements for very large unannotated tuples#22841
AlexWaygood merged 1 commit intomainfrom
alex/big-slow-tuples-2

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Jan 24, 2026

Fixes astral-sh/ty#71. Stacked on top of #22842.

This yields a huge speedup locally when checking https://github.com/MMD-Blender/blender_mmd_tools/blob/6ae13d6039763b6813832622c13da464983e93b3/mmd_tools/m17n.py. The pathological performance on that file wasn't due to inferring a type for the large tuple (that is very fast) -- it was inferring types when subscripting and unpacking that tuple that caused issues. If the tuple is inferred as containing many varied Literal types, subscription and unpacking operations on the tuple involve mapping type operations over huge unions. If the tuple is instead inferred as containing types like int, str and bytes, however, the unions inferred are much smaller.

Promoting Literal types in a very large tuple means that you don't get precise Literal types out of the tuple when you subscript it. But I think it's very unlikely that you'd want or need a precise Literal type when subscripting a tuple of >64 elements. And if you do... you can give us a type hint, and we'll still respect your intent. (There's some examples in the tests I've added: we'll even respect type hints like tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[65]], and we'll infer Literal types for only the first, second and last elements of the tuple.)

As Eric noted in astral-sh/ty#71 (comment), pyright also has a fallback like this to avoid pathological performance for large tuples, but pyright's fallback is much more lossy fallback: it falls back to tuple[Unknown, ...] for any tuple >256 elements long. I'd prefer not to do that unless we have to, and it seems to me that what I'm proposing here fixes all known pathological performance issues with large tuples without a significant sacrifice of precision.

@AlexWaygood AlexWaygood added performance Potential performance improvement ty Multi-file analysis & type inference labels Jan 24, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

Typing conformance results

No changes detected ✅

@AlexWaygood AlexWaygood changed the title Promote Literal types when inferring elements for very large unannotated tuples [ty] Promote Literal types when inferring elements for very large unannotated tuples Jan 24, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

mypy_primer results

Changes were detected when running on open source projects
prefect (https://github.com/PrefectHQ/prefect)
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[Any, Any] | int | dict[str, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `int | dict[Any, Any] | float | ... omitted 3 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `dict[Any, Any] | int | dict[str, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `int | dict[Any, Any] | float | ... omitted 3 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Argument type `dict[Any, Any] | int | dict[str, Any] | ... omitted 4 union elements` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `Unknown | dict[str, Any]` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `Unknown | int | dict[str, Any] | ... omitted 4 union elements` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | dict[str, Any]]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | int | dict[str, Any] | ... omitted 4 union elements]`
- src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown]`
+ src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | int | float | ... omitted 4 union elements]`
- src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown]`
+ src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | int | float | ... omitted 4 union elements]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Argument type `str | dict[str, Any]` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Argument type `str | int | dict[str, Any] | ... omitted 3 union elements` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
+ src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `int | Unknown | float | ... omitted 4 union elements`
- Found 5368 diagnostics
+ Found 5374 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 46 diagnostics
+ Found 47 diagnostics

pwndbg (https://github.com/pwndbg/pwndbg)
- pwndbg/aglib/kernel/__init__.py:264:20: error[unsupported-operator] Operator `+` is not supported between objects of type `Unknown | None` and `Literal[1]`
+ pwndbg/aglib/kernel/__init__.py:264:20: error[unsupported-operator] Operator `+` is not supported between objects of type `None | Unknown` and `Literal[1]`
- pwndbg/aglib/kernel/__init__.py:270:22: error[unsupported-operator] Operator `+` is not supported between objects of type `Unknown | None` and `int`
+ pwndbg/aglib/kernel/__init__.py:270:22: error[unsupported-operator] Operator `+` is not supported between objects of type `None | Unknown` and `int`

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/chain/decoding/tools.py:96:44: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- rotkehlchen/chain/decoding/tools.py:97:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress`, found `A@BaseDecoderTools`
+ rotkehlchen/chain/decoding/tools.py:99:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `Sequence[A@BaseDecoderTools]`, found `Unknown | tuple[BTCAddress, ...] | tuple[ChecksumAddress, ...] | tuple[SubstrateAddress, ...] | tuple[SolanaAddress, ...]`
- rotkehlchen/chain/decoding/tools.py:98:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress | None`, found `A@BaseDecoderTools | None`
+ rotkehlchen/chain/decoding/tools.py:100:62: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 2057 diagnostics
+ Found 2058 diagnostics

No memory usage changes detected ✅

@AlexWaygood AlexWaygood force-pushed the alex/big-slow-tuples-2 branch 2 times, most recently from 0688102 to 4caefe8 Compare January 24, 2026 23:40
@AlexWaygood AlexWaygood changed the base branch from main to alex/big-tuple-bench January 24, 2026 23:40
@AlexWaygood AlexWaygood reopened this Jan 24, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 24, 2026

Merging this PR will improve performance by ×4.8

⚡ 1 improved benchmark
✅ 23 untouched benchmarks
⏩ 30 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ty_micro[very_large_tuple] 334.5 ms 69.8 ms ×4.8

Comparing alex/big-slow-tuples-2 (262acd5) with main (2e7b74d)

Open in CodSpeed

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@AlexWaygood AlexWaygood force-pushed the alex/big-slow-tuples-2 branch 3 times, most recently from 689c00c to e47926e Compare January 25, 2026 10:39
@AlexWaygood AlexWaygood marked this pull request as ready for review January 25, 2026 10:40
Comment on lines +9850 to +9853
/// If a tuple literal has more elements than this constant,
/// we promote `Literal` types when inferring the elements of the tuple.
/// This provides a huge speedup on files that have very large unannotated tuple literals.
const MAX_TUPLE_LENGTH_FOR_UNANNOTATED_LITERAL_INFERENCE: usize = 64;
Copy link
Member Author

@AlexWaygood AlexWaygood Jan 25, 2026

Choose a reason for hiding this comment

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

64 is a small number[citation needed], but I think it should easily be big enough. I wondered about setting it even lower, e.g. 32. I think there are vanishingly few cases where it's useful to have Literal types preserved for the subelements of a large tuple.

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 looks good to me, and I'm fine just going with it.

I do wonder if we couldn't get the same speedup by applying the literal promotion at the point where we are unioning all the tuple members, rather than when we construct the tuple type? Then you'd still get precise type on subscript, just not on iteration/unpacking (where we need the homogenized type).

@AlexWaygood
Copy link
Member Author

I do wonder if we couldn't get the same speedup by applying the literal promotion at the point where we are unioning all the tuple members, rather than when we construct the tuple type? Then you'd still get precise type on subscript, just not on iteration/unpacking (where we need the homogenized type).

I think that's possible, yeah. But I'm still not sure what the actual use case is? I can't think of a situation where I'd benefit from getting a precise Literal type from indexing into a tuple of >64 elements; I think it's almost always irrelevant to have types that precise for a tuple that long. And I think your proposal would complicate the implementation a little. So I'm inclined to leave it as it is for now. We can always iterate on it later if we get complaints ;)

Base automatically changed from alex/big-tuple-bench to main January 27, 2026 21:03
@AlexWaygood AlexWaygood force-pushed the alex/big-slow-tuples-2 branch from e47926e to 262acd5 Compare January 27, 2026 21:04
@AlexWaygood AlexWaygood enabled auto-merge (squash) January 27, 2026 21:04
@AlexWaygood AlexWaygood merged commit 500fed6 into main Jan 27, 2026
48 checks passed
@AlexWaygood AlexWaygood deleted the alex/big-slow-tuples-2 branch January 27, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Potential performance improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ty is slow for large tuples

2 participants