[ty] Create fresh copies of generic callable typevars by dcreager · Pull Request #24949 · astral-sh/ruff (original) (raw)
Merged
merged 32 commits into
Jun 8, 2026
Conversation
A generic callable binds new typevars. If that callable is used more than once in a particular expression, we should create separate (aka "fresh") copies of those typevars, so that the inferred types for each do not conflict with each other.
This comes up in two situations:
- We might invoke a generic function recursively from inside of its body. In that case, the call site introduces fresh inferable copies of the function's typevars, which should not conflict with the existing non-inferable copies from the function signature.
- We might pass the same generic callable more than once to some other call. In this case, each occurrence should have distinct copies of its typevars, that do not need to unify with each other. This can occur whenever we compare a generic callable signature for assignability with some other signature.
This PR updates these two places (call binding and signature assignability checking) to introduce fresh copies of the callable's typevars when needed.
Typing conformance results
No changes detected ✅
Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 92.16%. The percentage of expected errors that received a diagnostic held steady at 87.31%. The number of fully passing files held steady at 92/134.
Memory usage report
Summary
| Project | Old | New | Diff | Outcome |
|---|---|---|---|---|
| prefect | 564.13MB | 564.64MB | +0.09% (520.04kB) | ⏫ |
| sphinx | 207.45MB | 207.60MB | +0.07% (145.96kB) | ⏫ |
| trio | 87.82MB | 87.87MB | +0.06% (52.14kB) | ⏫ |
| flake8 | 35.36MB | 35.37MB | +0.03% (9.24kB) | ⏫ |
Significant changes
Click to expand detailed breakdown
prefect
| Name | Old | New | Diff | Outcome |
|---|---|---|---|---|
| BoundTypeVarInstance | 1.52MB | 1.70MB | +11.80% (183.16kB) | ⏫ |
| is_redundant_with_impl | 1.93MB | 1.99MB | +2.71% (53.61kB) | ⏫ |
| InferableTypeVarsInner | 185.24kB | 230.27kB | +24.31% (45.03kB) | ⏫ |
| when_constraint_set_assignable_to_owned_impl | 3.04MB | 3.07MB | +1.00% (31.14kB) | ⏫ |
| GenericContext | 305.89kB | 334.38kB | +9.31% (28.48kB) | ⏫ |
| infer_expression_types_impl | 55.18MB | 55.21MB | +0.05% (26.02kB) | ⏫ |
| Type<'db>::apply_specialization_inner_::interned_arguments | 2.78MB | 2.80MB | +0.71% (20.31kB) | ⏫ |
| infer_definition_types | 75.91MB | 75.93MB | +0.03% (19.95kB) | ⏫ |
| Type<'db>::apply_specialization_inner_ | 3.17MB | 3.19MB | +0.55% (17.89kB) | ⏫ |
| Specialization | 2.50MB | 2.51MB | +0.56% (14.28kB) | ⏫ |
| infer_scope_types_impl | 47.63MB | 47.64MB | +0.02% (8.91kB) | ⏫ |
| is_redundant_with_impl::interned_arguments | 2.36MB | 2.37MB | +0.36% (8.59kB) | ⏫ |
| CallableType | 2.71MB | 2.72MB | +0.29% (8.17kB) | ⏫ |
| inferable_typevars_inner | 75.28kB | 83.18kB | +10.50% (7.91kB) | ⏫ |
| StaticClassLiteral<'db>::variance_of_::interned_arguments | 85.92kB | 90.98kB | +5.89% (5.06kB) | ⏫ |
| ... | 34 more |
sphinx
| Name | Old | New | Diff | Outcome |
|---|---|---|---|---|
| BoundTypeVarInstance | 624.16kB | 695.78kB | +11.47% (71.62kB) | ⏫ |
| when_constraint_set_assignable_to_owned_impl | 1.49MB | 1.50MB | +0.78% (11.88kB) | ⏫ |
| InferableTypeVarsInner | 76.11kB | 87.45kB | +14.90% (11.34kB) | ⏫ |
| GenericContext | 140.11kB | 149.59kB | +6.77% (9.48kB) | ⏫ |
| infer_expression_types_impl | 20.48MB | 20.49MB | +0.03% (6.55kB) | ⏫ |
| infer_definition_types | 20.72MB | 20.72MB | +0.03% (5.45kB) | ⏫ |
| is_redundant_with_impl | 914.20kB | 919.57kB | +0.59% (5.37kB) | ⏫ |
| Type<'db>::apply_specialization_inner_::interned_arguments | 1.39MB | 1.40MB | +0.37% (5.31kB) | ⏫ |
| Type<'db>::apply_specialization_inner_ | 1.51MB | 1.52MB | +0.30% (4.69kB) | ⏫ |
| Specialization | 1.26MB | 1.27MB | +0.30% (3.86kB) | ⏫ |
| inferable_typevars_inner | 36.83kB | 38.69kB | +5.05% (1.86kB) | ⏫ |
| is_redundant_with_impl::interned_arguments | 1.14MB | 1.14MB | +0.12% (1.38kB) | ⏫ |
| StaticClassLiteral<'db>::try_mro_ | 2.23MB | 2.23MB | +0.06% (1.30kB) | ⏫ |
| infer_statement_types_impl | 460.65kB | 461.46kB | +0.18% (828.00B) | ⏫ |
| bound_typevar_default_type | 16.15kB | 16.92kB | +4.74% (784.00B) | ⏫ |
| ... | 15 more |
trio
| Name | Old | New | Diff | Outcome |
|---|---|---|---|---|
| BoundTypeVarInstance | 164.18kB | 183.83kB | +11.97% (19.65kB) | ⏫ |
| InferableTypeVarsInner | 64.82kB | 74.35kB | +14.70% (9.53kB) | ⏫ |
| GenericContext | 126.41kB | 133.42kB | +5.55% (7.01kB) | ⏫ |
| when_constraint_set_assignable_to_owned_impl | 401.11kB | 405.18kB | +1.02% (4.08kB) | ⏫ |
| CallableType | 666.52kB | 668.57kB | +0.31% (2.05kB) | ⏫ |
| infer_definition_types | 6.61MB | 6.61MB | +0.02% (1.42kB) | ⏫ |
| inferable_typevars_inner | 28.08kB | 29.27kB | +4.26% (1.20kB) | ⏫ |
| FunctionType | 695.29kB | 696.35kB | +0.15% (1.06kB) | ⏫ |
| StaticClassLiteral<'db>::try_mro_ | 714.82kB | 715.82kB | +0.14% (1016.00B) | ⏫ |
| bound_typevar_default_type | 14.12kB | 15.00kB | +6.20% (896.00B) | ⏫ |
| FunctionType<'db>::signature_ | 745.40kB | 746.04kB | +0.09% (656.00B) | ⏫ |
| Specialization | 455.38kB | 455.95kB | +0.13% (592.00B) | ⏫ |
| cached_protocol_interface | 140.38kB | 140.93kB | +0.39% (564.00B) | ⏫ |
| Type<'db>::apply_specialization_inner_ | 558.77kB | 559.16kB | +0.07% (396.00B) | ⏫ |
| GenericAlias | 186.05kB | 186.40kB | +0.19% (360.00B) | ⏫ |
| ... | 9 more |
flake8
| Name | Old | New | Diff | Outcome |
|---|---|---|---|---|
| BoundTypeVarInstance | 45.70kB | 50.78kB | +11.11% (5.08kB) | ⏫ |
| GenericContext | 44.95kB | 46.52kB | +3.48% (1.56kB) | ⏫ |
| when_constraint_set_assignable_to_owned_impl | 157.64kB | 159.03kB | +0.88% (1.38kB) | ⏫ |
| InferableTypeVarsInner | 22.81kB | 24.02kB | +5.33% (1.21kB) | ⏫ |
ecosystem-analyzer results
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
| invalid-argument-type | 3 | 0 | 1 |
| Total | 3 | 0 | 1 |
Raw diff:
Expression (https://github.com/cognitedata/Expression)
- expression/collections/maptree.py:176:36 error[invalid-argument-type] Argument to function
try_findis incorrect: ExpectedOption[MapTreeLeaf[Key@try_find, Value@try_find]], foundOption[MapTreeLeaf[Never, Never]]
xarray (https://github.com/pydata/xarray)
- xarray/backends/api.py:1363:42 error[invalid-argument-type] Argument to function
_remove_pathis incorrect: ExpectedNestedSequence[_FLike@_remove_path], found(_FLike@_remove_path & Top[list[Unknown]]) | (NestedSequence[_FLike@_remove_path] & Top[list[Unknown]]) - xarray/core/utils.py:356:35 error[invalid-argument-type] Argument to function
flat_itemsis incorrect: ExpectedMapping[str, dict[str, Divergent] | Unknown], founddict[str, Divergent] | (T@flat_items & Top[dict[Unknown, Unknown]])
- xarray/structure/combine.py:69:57 error[invalid-argument-type] Argument to function
_infer_tile_ids_from_nested_listis incorrect: ExpectedNestedSequence[Unknown], foundobject
- xarray/structure/combine.py:69:57 error[invalid-argument-type] Argument to function
_infer_tile_ids_from_nested_listis incorrect: ExpectedNestedSequence[T@_infer_tile_ids_from_nested_list], foundobject
Full report with detailed diff (timing results)
Merging this PR will degrade performance by 7.38%
⚠️ Different runtime environments detected
Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.
❌ 2 (👁 2) regressed benchmarks✅ 65 untouched benchmarks⏩ 60 skipped benchmarks1
Performance Changes
| Mode | Benchmark | BASE | HEAD | Efficiency | |
|---|---|---|---|---|---|
| 👁 | WallTime | pydantic | 9.9 s | 10.5 s | -5.76% |
| 👁 | Simulation | DateType | 226.7 ms | 249.1 ms | -8.98% |
Comparing dcreager/alpha-renaming (dca5384) with main (b5fce66)
Footnotes
- 60 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. ↩
jelle-openai added a commit to jelle-openai/ruff that referenced this pull request
This was referenced
May 28, 2026
All of the ecosystem changes are exposing existing feature gaps in new locations. Though the changed diagnostic in xarray does show the intended fix from this PR — we now see distinct uses of the typevar of this recursive function.
The reduced performance seems reasonable for the new (and necessary) work that's being performed. There are similar slowdowns in a handful of ecosystem projects, but most of them are in line with the previous timing numbers.
dcreager marked this pull request as ready for review
| /// Binds a legacy typevar with the generic context (class, function, type alias) that it is |
|---|
| /// being used in. |
| BindLegacyTypevars(BindingContext<'db>), |
| /// Freshens typevars bound by a generic context occurrence by adding a shared delta. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is a "shared delta"? I guess I might've considered this the "nonce" itself? Can we give it a dedicated type?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're adding a value to the existing nonce to make sure that the result is unique. So if we have
Callable[[T'0, T'1], T'2]
(where each T is the same typevar, but with different nonces), we need to bump everything by 3 to make sure that the result doesn't conflict with any of the existing nonces:
Callable[[T'3, T'4], T'5]
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense to me from reading the code and the summary, though I again don't feel super well positioned to give substantive feedback. I think that's fine, just want to call it out.
Should "freshen" just be "copy" or something?
I'm a little saddened by the memory increase here, though I can also try to reduce that hit separately.
I think "freshen" is the terminology mypy uses for this too internally FWIW — don't know if that's something @dcreager was aware of before this PR or if he came to the same terminology independently 😆
If it's used elsewhere no objection from me for sure
Codex gave me this for reducing memory significantly: #25670
Codex gave me this for reducing memory significantly: #25670
Nice! Merged into here
I think "freshen" is the terminology mypy uses for this too internally FWIW — don't know if that's something @dcreager was aware of before this PR or if he came to the same terminology independently 😆
I didn't know about mypy using it, but it's a common term in the literature for this too!
dcreager deleted the dcreager/alpha-renaming branch
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
[ Show hidden characters]({{ revealButtonHref }})
Labels
Multi-file analysis & type inference