Skip to content

[ty] Fix assignability of intersections with bounded typevars#24193

Closed
carljm wants to merge 1 commit intomainfrom
cjm/tvinter
Closed

[ty] Fix assignability of intersections with bounded typevars#24193
carljm wants to merge 1 commit intomainfrom
cjm/tvinter

Conversation

@carljm
Copy link
Copy Markdown
Contributor

@carljm carljm commented Mar 25, 2026

Summary

Fixes astral-sh/ty#3137

We already supported assigning a bounded/constrained typevar to a super-type of its bound / the union of its constraints. But this handling was not applied in case the typevar was part of an intersection; this PR fixes that.

The expansion applies only to non-inferable typevars -- if we applied it to inferable typevars, we'd miss finding their constraints.

There's some code here for NewType also, but it doesn't handle any new cases, just maintains the existing support.

Test Plan

Added mdtests.

Non-flaky ecosystem results are all cases of the expected false positives going away.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Mar 25, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Mar 25, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 86.46%. The percentage of expected errors that received a diagnostic held steady at 80.68%. The number of fully passing files held steady at 67/132.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Mar 25, 2026

Memory usage report

Summary

Project Old New Diff Outcome
sphinx 262.42MB 262.51MB +0.03% (84.02kB)
prefect 705.08MB 705.16MB +0.01% (79.27kB)
trio 115.74MB 115.75MB +0.02% (18.21kB)
flake8 47.94MB 47.94MB +0.00% (912.00B)

Significant changes

Click to expand detailed breakdown

sphinx

Name Old New Diff Outcome
is_redundant_with_impl 1.78MB 1.84MB +3.19% (58.24kB)
all_narrowing_constraints_for_expression 2.48MB 2.50MB +0.66% (16.70kB)
all_negative_narrowing_constraints_for_expression 1.08MB 1.08MB +0.38% (4.16kB)
infer_expression_types_impl 19.69MB 19.69MB +0.01% (2.71kB)
infer_scope_types_impl 15.52MB 15.52MB +0.01% (1.70kB)
infer_definition_types 24.13MB 24.13MB +0.00% (528.00B)

prefect

Name Old New Diff Outcome
is_redundant_with_impl 5.51MB 5.57MB +1.07% (60.16kB)
all_narrowing_constraints_for_expression 7.42MB 7.43MB +0.10% (7.56kB)
infer_expression_types_impl 56.21MB 56.21MB +0.01% (3.79kB)
infer_scope_types_impl 53.65MB 53.65MB +0.01% (2.79kB)
all_negative_narrowing_constraints_for_expression 2.81MB 2.81MB +0.07% (1.95kB)
infer_definition_types 89.86MB 89.87MB +0.00% (1.73kB)
infer_expression_type_impl 14.05MB 14.05MB +0.01% (792.00B)
Type<'db>::member_lookup_with_policy_ 16.31MB 16.31MB +0.00% (348.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 10.11MB 10.11MB +0.00% (156.00B)
TupleType<'db>::to_class_type_ 491.85kB 491.87kB +0.00% (24.00B)

trio

Name Old New Diff Outcome
is_redundant_with_impl 469.91kB 485.75kB +3.37% (15.84kB)
infer_expression_types_impl 6.04MB 6.04MB +0.02% (972.00B)
all_narrowing_constraints_for_expression 626.97kB 627.76kB +0.13% (804.00B)
all_negative_narrowing_constraints_for_expression 210.86kB 211.26kB +0.19% (408.00B)
infer_scope_types_impl 4.76MB 4.76MB +0.00% (216.00B)
UnionType<'db>::from_two_elements_ 280.75kB 280.78kB +0.01% (24.00B)

flake8

Name Old New Diff Outcome
is_redundant_with_impl 136.72kB 137.54kB +0.60% (840.00B)
all_narrowing_constraints_for_expression 102.34kB 102.37kB +0.03% (36.00B)
all_negative_narrowing_constraints_for_expression 45.39kB 45.42kB +0.05% (24.00B)
infer_definition_types 1.92MB 1.92MB +0.00% (12.00B)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Mar 25, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-await 40 0 0
invalid-argument-type 0 11 0
invalid-return-type 1 0 0
Total 41 11 0

Large timing changes:

Project Old Time New Time Change
egglog-python 0.49s 1.56s +216%
pandera 0.77s 1.87s +143%
mitmproxy 0.34s 0.51s +52%

Changes in flaky projects detected. Raw diff output excludes flaky projects; see the HTML report for details.

Raw diff:

Tanjun (https://github.com/FasterSpeeding/Tanjun)
- tanjun/annotations.py:1949:74 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int | float`, found `_NumberT@__getitem__ & ~int`
- tanjun/annotations.py:1996:74 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int | float`, found `_NumberT@__getitem__ & ~int`
- tanjun/annotations.py:2110:75 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int | float`, found `_NumberT@__getitem__ & ~float`
- tanjun/annotations.py:2110:86 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int | float`, found `_NumberT@__getitem__ & ~float`
- tanjun/annotations.py:2724:26 error[invalid-argument-type] Argument to function `parse_annotated_args` is incorrect: Expected `SlashCommand[Any] | MessageCommand[Any]`, found `_CommandUnionT@with_annotated_args & ~AlwaysFalsy`

altair (https://github.com/vega/altair)
- altair/vegalite/v6/api.py:1179:45 error[invalid-argument-type] Argument to function `_reveal_parsed_shorthand` is incorrect: Expected `Mapping[str, Any]`, found `_C@Then & Top[dict[Unknown, Unknown]]`

artigraph (https://github.com/artigraph/artigraph)
- src/arti/internal/type_hints.py:177:37 error[invalid-argument-type] Argument to function `_check_issubclass` is incorrect: Expected `type`, found `T@lenient_issubclass & ~tuple[object, ...]`

beartype (https://github.com/beartype/beartype)
- beartype/_decor/_nontype/decornontype.py:161:43 error[invalid-argument-type] Argument is incorrect: Argument type `BeartypeableT@beartype_nontype & ~type` does not satisfy upper bound `((...) -> Any) | classmethod[Unknown, (...), Unknown] | property` of type variable `BeartypeableT`
- beartype/_decor/_nontype/decornontype.py:220:47 error[invalid-argument-type] Argument is incorrect: Argument type `BeartypeableT@beartype_nontype & ~type` does not satisfy upper bound `((...) -> Any) | classmethod[Unknown, (...), Unknown] | property` of type variable `BeartypeableT`
- beartype/_decor/decorcore.py:133:26 error[invalid-argument-type] Argument to function `beartype_nontype` is incorrect: Argument type `BeartypeableT@_beartype_object_fatal & ~type` does not satisfy upper bound `((...) -> Any) | classmethod[Unknown, (...), Unknown] | property` of type variable `BeartypeableT`

setuptools (https://github.com/pypa/setuptools)
- setuptools/glob.py:75:42 error[invalid-argument-type] Argument to function `has_magic` is incorrect: Expected `str | bytes`, found `AnyStr@_iglob & ~AlwaysFalsy`

Full report with detailed diff (timing results)

@carljm carljm marked this pull request as ready for review March 25, 2026 23:19
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 25, 2026

Merging this PR will degrade performance by 83.24%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 6 regressed benchmarks
✅ 21 untouched benchmarks
⏩ 30 skipped benchmarks1

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

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ty_micro[large_union_narrowing] 498.2 ms 2,973.5 ms -83.24%
WallTime pandas 69.6 s 72.7 s -4.26%
WallTime altair 5.1 s 5.7 s -9.91%
WallTime colour_science 72.5 s 77.7 s -6.69%
WallTime multithreaded 1.3 s 1.5 s -14.04%
Simulation hydra-zen 1.1 s 1.2 s -6.32%

Comparing cjm/tvinter (04da369) with main (55c5d90)

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.

@carljm carljm marked this pull request as draft March 26, 2026 04:23
@carljm
Copy link
Copy Markdown
Contributor Author

carljm commented Mar 26, 2026

Looks like this needs some perf work!

Comment on lines +593 to +610
let expanded = intersection.map_positive(db, |element| match *element {
Type::TypeVar(typevar) if !typevar.is_inferable(db, self.inferable) => {
// Leave inferable typevars alone: widening them here could bypass the normal
// solving path and discard information needed to infer a concrete specialization.
match typevar.typevar(db).bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound,
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.as_type(db)
}
None => *element,
}
}
_ => element
.as_new_type()
.map_or(*element, |newtype| newtype.concrete_base_type(db)),
});

(expanded != Type::Intersection(intersection)).then_some(expanded)
Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood Mar 26, 2026

Choose a reason for hiding this comment

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

a pre-check may improve performance here. And, I added a helper method for exactly this expansion the other day, which you can use here.

Suggested change
let expanded = intersection.map_positive(db, |element| match *element {
Type::TypeVar(typevar) if !typevar.is_inferable(db, self.inferable) => {
// Leave inferable typevars alone: widening them here could bypass the normal
// solving path and discard information needed to infer a concrete specialization.
match typevar.typevar(db).bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound,
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.as_type(db)
}
None => *element,
}
}
_ => element
.as_new_type()
.map_or(*element, |newtype| newtype.concrete_base_type(db)),
});
(expanded != Type::Intersection(intersection)).then_some(expanded)
if intersection.positive(db).iter().any(|t| matches!(t, Type::NewTypeInstance(_) | Type::TypeVar(_)) {
Some(intersection.with_expanded_typevars_and_newtypes(db))
} else {
None
}

@carljm
Copy link
Copy Markdown
Contributor Author

carljm commented Apr 9, 2026

Closing in favor of #24502

@carljm carljm closed this Apr 9, 2026
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.

Generic with bound unassignable to its bound after widening and narrowing

3 participants