Skip to content

[ty] increase the limit on the number of elements in a non-recursively defined literal union#21683

Merged
carljm merged 4 commits intoastral-sh:mainfrom
mtshiba:faster-literal-type-convergence
Dec 5, 2025
Merged

[ty] increase the limit on the number of elements in a non-recursively defined literal union#21683
carljm merged 4 commits intoastral-sh:mainfrom
mtshiba:faster-literal-type-convergence

Conversation

@mtshiba
Copy link
Collaborator

@mtshiba mtshiba commented Nov 28, 2025

Summary

Closes astral-sh/ty#957

As explained in astral-sh/ty#957, literal union types for recursively defined values ​​can be widened early to speed up the convergence of fixed-point iterations.
This PR achieves this by embedding a marker in UnionType that distinguishes whether a value is recursively defined.

This also allows us to identify values ​​that are not recursively defined, so I've increased the limit on the number of elements in a literal union type for such values.

Edit: while this PR doesn't provide the significant performance improvement initially hoped for, it does have the benefit of allowing the number of elements in a literal union to be raised above the salsa limit, and indeed mypy_primer results revealed that a literal union of 220 elements was actually being used.

Test Plan

call/union.md has been updated

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 28, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Nov 28, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 28, 2025

mypy_primer results

Changes were detected when running on open source projects
pandera (https://github.com/pandera-dev/pandera)
- pandera/api/pandas/model.py:210:13: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | str | ExtensionDtype | ... omitted 3 union elements`, found `dict[Unknown, Unknown | None]`
+ pandera/api/pandas/model.py:210:13: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `dict[Unknown, Unknown | None]`
+ pandera/engines/numpy_engine.py:52:41: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `str`
- pandera/engines/pandas_engine.py:1962:26: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | str | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
+ pandera/engines/pandas_engine.py:1962:26: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
- pandera/engines/pandas_engine.py:1988:26: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | str | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
+ pandera/engines/pandas_engine.py:1988:26: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
- pandera/engines/pyarrow_engine.py:440:22: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | str | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
+ pandera/engines/pyarrow_engine.py:440:22: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
- pandera/engines/pyarrow_engine.py:465:22: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | str | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
+ pandera/engines/pyarrow_engine.py:465:22: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `ArrowDtype | None`
- tests/pandas/test_pydantic.py:308:53: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | str | ExtensionDtype | ... omitted 3 union elements`, found `dict[Unknown, Unknown] | dict[str | Unknown, Any | None | type[Any]]`
+ tests/pandas/test_pydantic.py:308:53: error[invalid-argument-type] Argument to bound method `astype` is incorrect: Expected `type | Literal["bool", "boolean", "?", "b1", "bool_", ... omitted 220 literals] | ExtensionDtype | ... omitted 3 union elements`, found `dict[Unknown, Unknown] | dict[str | Unknown, Any | None | type[Any]]`
- Found 1635 diagnostics
+ Found 1636 diagnostics

freqtrade (https://github.com/freqtrade/freqtrade)
- freqtrade/rpc/rpc.py:1451:88: error[invalid-argument-type] Argument to bound method `replace` is incorrect: Expected `Sequence[str | bytes | date | ... omitted 10 union elements] | NAType | date | ... omitted 13 union elements`, found `dict[Unknown | NaTType, Unknown | None]`
+ freqtrade/rpc/rpc.py:1448:28: error[no-matching-overload] No overload of bound method `select_dtypes` matches arguments

openlibrary (https://github.com/internetarchive/openlibrary)
+ openlibrary/plugins/worksearch/code.py:1207:9: error[invalid-argument-type] Argument to function `run_solr_query` is incorrect: Expected `Literal["UNLABELLED", "BOOK_SEARCH", "BOOK_SEARCH_API", "BOOK_SEARCH_FACETS", "BOOK_CAROUSEL", ... omitted 11 literals]`, found `str`
- Found 1122 diagnostics
+ Found 1123 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/_logging.py:153:13: warning[unsupported-base] Unsupported class base with type `<class 'Mapping[str, Style]'> | <class 'Mapping[str, Divergent]'>`
- Found 42 diagnostics
+ Found 41 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ tests/indexes/test_index_float.py:113:13: error[type-assertion-failure] Type `Index[int | float]` does not match asserted type `Index[Any]`
- tests/series/test_series_float.py:52:13: error[type-assertion-failure] Type `Series[int | float]` does not match asserted type `Series[Any]`
+ tests/series/test_series_float.py:52:13: error[type-assertion-failure] Type `Series[int | float]` does not match asserted type `Series[list[Unknown | float]]`
+ tests/series/test_series_float.py:108:13: error[type-assertion-failure] Type `Series[int | float]` does not match asserted type `Unknown`
+ tests/series/test_series_float.py:108:25: error[no-matching-overload] No overload of bound method `astype` matches arguments
+ tests/series/test_series_float.py:110:15: error[no-matching-overload] No overload of bound method `astype` matches arguments
- Found 5859 diagnostics
+ Found 5863 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- pydantic/fields.py:943:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:943:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:983:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:983:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1026:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1026:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1066:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1066:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1109:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1109:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1148:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1148:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1188:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1188:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1567:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1567:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`

No memory usage changes detected ✅

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 28, 2025

CodSpeed Performance Report

Merging #21683 will degrade performances by 9.91%

Comparing mtshiba:faster-literal-type-convergence (a515490) with main (ecab623)

Summary

❌ 1 regression
✅ 21 untouched
⏩ 30 skipped1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Mode Benchmark BASE HEAD Change
Simulation ty_micro[many_string_assignments] 76.4 ms 84.8 ms -9.91%

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.

@mtshiba
Copy link
Collaborator Author

mtshiba commented Nov 29, 2025

Hmm, for example, when inspecting the following code, I see a clear performance improvement locally, but the change on codspeed seems less clear:

class Counter:
    def __init__(self: "Counter"):
        self.count = 0

    def increment(self: "Counter"):
        self.count = self.count + 1

reveal_type(Counter().count)  # revealed: Unknown | int

main:

# release
$ hyperfine -i "ty check widen.py"
Benchmark 1: ty check widen.py
  Time (mean ± σ):     103.3 ms ±   8.6 ms    [User: 56.9 ms, System: 45.0 ms]
  Range (min … max):    96.4 ms … 136.6 ms    22 runs

# debug
$ hyperfine -i "ty check widen.py"
Benchmark 1: ty check widen.py
  Time (mean ± σ):     899.9 ms ±   6.2 ms    [User: 849.7 ms, System: 51.9 ms]
  Range (min … max):   889.4 ms … 910.3 ms    10 runs

#21683:

# release
$ hyperfine -i "ty check widen.py"
Benchmark 1: ty check widen.py
  Time (mean ± σ):      59.6 ms ±   9.2 ms    [User: 26.5 ms, System: 37.8 ms]
  Range (min … max):    52.5 ms …  95.7 ms    31 runs

# debug
$ hyperfine -i "ty check widen.py"
Benchmark 1: ty check widen.py
  Time (mean ± σ):     267.2 ms ±   4.1 ms    [User: 222.2 ms, System: 55.6 ms]
  Range (min … max):   261.6 ms … 276.5 ms    10 runs

@mtshiba
Copy link
Collaborator Author

mtshiba commented Nov 29, 2025

Conversely, the apparent performance degradation is due to increasing MAX_NON_RECURSIVE_UNION_LITERALS.
Even setting it to 512 did not result in a panic, which shows that the divergent cases are being correctly avoided.

The performance impact of changing MAX_NON_RECURSIVE_UNION_LITERALS is as follows:

190: https://codspeed.io/astral-sh/ruff/runs/compare/6929edd91e804897641888e6..6929fa9ed77b771deb1a9244
256: https://codspeed.io/astral-sh/ruff/runs/compare/6929edd91e804897641888e6..6929f4ded77b771deb1a9211
512: https://codspeed.io/astral-sh/ruff/runs/compare/6929edd91e804897641888e6..6929ef181e804897641888ec

@MichaReiser
Copy link
Member

Hmm, for example, when inspecting the following code, I see a clear performance improvement locally, but the change on codspeed seems less clear:

There might just not be enough very large unions so that, in the grand picture, the improvement is lost in noise

@mtshiba mtshiba changed the title [ty] WIP: accelerate fixed-point convergence of recursively defined literal types [ty] increase the limit on the number of elements in a non-recursively defined literal union Dec 4, 2025
@mtshiba
Copy link
Collaborator Author

mtshiba commented Dec 4, 2025

I think this is ready for review. Here are some comments:

The performance degradation of codspeed(many_string_assignments) is entirely expected.

There is no clear standard for what the new upper limit for literal unions should be, but I've set it to 256 for now.
mypy_primer revealed that literal unions of 220 elements are actually used, but said that of 512 elements is likely not present in practice.

@mtshiba mtshiba marked this pull request as ready for review December 4, 2025 10:49
@AlexWaygood AlexWaygood removed their request for review December 4, 2025 11:11
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 great, thank you!

@carljm carljm merged commit f3e5713 into astral-sh:main Dec 5, 2025
40 of 41 checks passed
@mtshiba mtshiba deleted the faster-literal-type-convergence branch December 5, 2025 02:20
dcreager added a commit that referenced this pull request Dec 5, 2025
* origin/main: (41 commits)
  [ty] Carry generic context through when converting class into `Callable` (#21798)
  [ty] Add more tests for renamings (#21810)
  [ty] Minor improvements to `assert_type` diagnostics (#21811)
  [ty] Add some attribute/method renaming test cases (#21809)
  Update mkdocs-material to 9.7.0 (Insiders now free) (#21797)
  Remove unused whitespaces in test cases (#21806)
  [ty] fix panic when instantiating a type variable with invalid constraints (#21663)
  [ty] fix build failure caused by conflicts between #21683 and #21800 (#21802)
  [ty] do nothing with `store_expression_type` if `inner_expression_inference_state` is `Get` (#21718)
  [ty] increase the limit on the number of elements in a non-recursively defined literal union (#21683)
  [ty] normalize typevar bounds/constraints in cycles (#21800)
  [ty] Update completion eval to include modules
  [ty] Add modules to auto-import
  [ty] Add support for module-only import requests
  [ty] Refactor auto-import symbol info
  [ty] Clarify the use of `SymbolKind` in auto-import
  [ty] Redact ranking of completions from e2e LSP tests
  [ty] Tweaks tests to use clearer language
  [ty] Update evaluation results
  [ty] Make auto-import ignore symbols in modules starting with a `_`
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

set a lower limit for size of literal unions we will do precise type inference of operations for

4 participants