Skip to content

Conversation

@dcreager
Copy link
Member

@dcreager dcreager commented Nov 11, 2025

Constraint sets can now track subtyping/assignability/etc of generic callables correctly. For instance:

def identity[T](t: T) -> T:
    return t

constraints = ConstraintSet.always()
static_assert(constraints.implies_subtype_of(TypeOf[identity], Callable[[int], int]))
static_assert(constraints.implies_subtype_of(TypeOf[identity], Callable[[str], str]))

A generic callable can be considered an intersection of all of its possible specializations, and an assignability check with an intersection as the lhs side succeeds of any of the intersected types satisfies the check. Put another way, if someone expects to receive any function with a signature of (int) -> int, we can give them identity.

Note that the corresponding check using is_subtype_of directly does not yet work, since #20093 has not yet hooked up the core typing relationship logic to use constraint sets:

# These currently fail
static_assert(is_subtype_of(TypeOf[identity], Callable[[int], int]))
static_assert(is_subtype_of(TypeOf[identity], Callable[[str], str]))

To do this, we add a new existential quantification operation on constraint sets. This takes in a list of typevars and removes those typevars from the constraint set. Conceptually, we return a new constraint set that evaluates to true when there was any assignment of the removed typevars that caused the old constraint set to evaluate to true.

When comparing a generic constraint set, we add its typevars to the inferable set, and figure out whatever constraints would allow any specialization to satisfy the check. We then use the new existential quantification operator to remove those new typevars, since the caller doesn't (and shouldn't) know anything about them.

@dcreager dcreager added internal An internal refactor or improvement ty Multi-file analysis & type inference labels Nov 11, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 11, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 11, 2025

mypy_primer results

Changes were detected when running on open source projects
pandas (https://github.com/pandas-dev/pandas)
- pandas/tests/series/test_cumulative.py:48:20: error[no-matching-overload] No overload matches arguments
- pandas/tests/series/test_cumulative.py:54:20: error[no-matching-overload] No overload matches arguments
- pandas/tests/series/test_cumulative.py:154:20: error[no-matching-overload] No overload matches arguments
- Found 3224 diagnostics
+ Found 3221 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/test/unit/test_frame.py:293:24: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Iterable[Iterable[Any]] | ndarray[Any, Any] | TypeBlocks | Frame | Series[Any, Any]`, found `None`
+ static_frame/test/unit/test_frame.py:293:24: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Iterable[Iterable[Any]] | TypeBlocks | Frame | Series[Any, Any]`, found `None`
- static_frame/test/unit/test_type_blocks.py:31:42: error[invalid-argument-type] Argument to bound method `from_blocks` is incorrect: Expected `ndarray[Any, Any] | Iterable[ndarray[Any, Any]]`, found `tuple[Literal[3], Literal[4]]`
+ static_frame/test/unit/test_type_blocks.py:31:42: error[invalid-argument-type] Argument to bound method `from_blocks` is incorrect: Expected `Iterable[ndarray[Any, Any]]`, found `tuple[Literal[3], Literal[4]]`

sympy (https://github.com/sympy/sympy)
+ sympy/polys/polyclasses.py:1293:31: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- sympy/utilities/tests/test_pickling.py:56:65: error[invalid-argument-type] Argument to function `dumps` is incorrect: Expected `int | None`, found `Unknown | int | ((x: _T@copy) -> _T@copy) | ((x: _T@deepcopy, memo: dict[int, Any] | None = None, _nil: Any = list[Unknown]) -> _T@deepcopy)`
+ sympy/utilities/tests/test_pickling.py:56:65: error[invalid-argument-type] Argument to function `dumps` is incorrect: Expected `int | None`, found `Unknown | int | ((x: _T@copy) -> _T@copy)`
- sympy/utilities/tests/test_pickling.py:74:46: error[invalid-argument-type] Argument to function `dumps` is incorrect: Expected `int | None`, found `(Unknown & ~(() -> object) & ~ModuleType) | (int & ~(() -> object)) | (((x: _T@copy) -> _T@copy) & ~(() -> object) & ~ModuleType) | (((x: _T@deepcopy, memo: dict[int, Any] | None = None, _nil: Any = list[Unknown]) -> _T@deepcopy) & ~(() -> object) & ~ModuleType)`
+ sympy/utilities/tests/test_pickling.py:74:46: error[invalid-argument-type] Argument to function `dumps` is incorrect: Expected `int | None`, found `(Unknown & ~(() -> object) & ~ModuleType) | (int & ~(() -> object)) | (((x: _T@copy) -> _T@copy) & ~(() -> object) & ~ModuleType)`
- Found 14599 diagnostics
+ Found 14600 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsGt & int`
+ pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `date` on TypedDict `DateSchema`: value of type `int`
- pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsGt & int`
+ pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsGt & int`
+ pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `time` on TypedDict `TimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsGt & int`
+ pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `int`
- pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsGt & int`
+ pydantic/experimental/pipeline.py:443:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `int`
- pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsGt & float`
+ pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `date` on TypedDict `DateSchema`: value of type `float`
- pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsGt & float`
+ pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsGt & float`
+ pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `float`
- pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `int` on TypedDict `IntSchema`: value of type `SupportsGt & float`
+ pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `int` on TypedDict `IntSchema`: value of type `float`
- pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsGt & float`
+ pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `float`
- pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsGt & float`
+ pydantic/experimental/pipeline.py:445:27: error[invalid-assignment] Invalid assignment to key "gt" with declared type `time` on TypedDict `TimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsGe & int`
+ pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `date` on TypedDict `DateSchema`: value of type `int`
- pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsGe & int`
+ pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsGe & int`
+ pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `int`
- pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsGe & int`
+ pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `int`
- pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsGe & int`
+ pydantic/experimental/pipeline.py:459:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `time` on TypedDict `TimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsGe & float`
+ pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `date` on TypedDict `DateSchema`: value of type `float`
- pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsGe & float`
+ pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `int` on TypedDict `IntSchema`: value of type `SupportsGe & float`
+ pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `int` on TypedDict `IntSchema`: value of type `float`
- pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsGe & float`
+ pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `float`
- pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsGe & float`
+ pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `float`
- pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsGe & float`
+ pydantic/experimental/pipeline.py:461:27: error[invalid-assignment] Invalid assignment to key "ge" with declared type `time` on TypedDict `TimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsLt & int`
+ pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `date` on TypedDict `DateSchema`: value of type `int`
- pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsLt & int`
+ pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsLt & int`
+ pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `int`
- pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsLt & int`
+ pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `time` on TypedDict `TimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsLt & int`
+ pydantic/experimental/pipeline.py:474:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `int`
- pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsLt & float`
+ pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsLt & float`
+ pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `float`
- pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsLt & float`
+ pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `float`
- pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsLt & float`
+ pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `date` on TypedDict `DateSchema`: value of type `float`
- pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsLt & float`
+ pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `time` on TypedDict `TimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `int` on TypedDict `IntSchema`: value of type `SupportsLt & float`
+ pydantic/experimental/pipeline.py:476:27: error[invalid-assignment] Invalid assignment to key "lt" with declared type `int` on TypedDict `IntSchema`: value of type `float`
- pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsLe & int`
+ pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `time` on TypedDict `TimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsLe & int`
+ pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `int`
- pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsLe & int`
+ pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `date` on TypedDict `DateSchema`: value of type `int`
- pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsLe & int`
+ pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `int`
- pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsLe & int`
+ pydantic/experimental/pipeline.py:489:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `int`
- pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `date` on TypedDict `DateSchema`: value of type `SupportsLe & float`
+ pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `date` on TypedDict `DateSchema`: value of type `float`
- pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `SupportsLe & float`
+ pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `datetime` on TypedDict `DatetimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `SupportsLe & float`
+ pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `timedelta` on TypedDict `TimedeltaSchema`: value of type `float`
- pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `time` on TypedDict `TimeSchema`: value of type `SupportsLe & float`
+ pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `time` on TypedDict `TimeSchema`: value of type `float`
- pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `int` on TypedDict `IntSchema`: value of type `SupportsLe & float`
+ pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `int` on TypedDict `IntSchema`: value of type `float`
- pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `SupportsLe & float`
+ pydantic/experimental/pipeline.py:491:27: error[invalid-assignment] Invalid assignment to key "le" with declared type `Decimal` on TypedDict `DecimalSchema`: value of type `float`

scipy (https://github.com/scipy/scipy)
- subprojects/highs/src/highspy/highs.py:1185:13: error[invalid-assignment] Object of type `(Iterable[highs_var | highs_linear_expression] & ndarray[object, dtype[object]]) | ndarray[Any, dtype[object_]]` is not assignable to `ndarray[Any, dtype[object_]]`
+ subprojects/highs/src/highspy/highs.py:1185:13: error[invalid-assignment] Object of type `ndarray[object, dtype[object]]` is not assignable to `ndarray[Any, dtype[object_]]`

No memory usage changes detected ✅

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 11, 2025

CodSpeed Performance Report

Merging #21392 will not alter performance

Comparing dcreager/coolable (d8f5876) with main (ac2d07e)

Summary

✅ 22 untouched
⏩ 30 skipped1

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.

@dcreager dcreager marked this pull request as ready for review November 14, 2025 14:01
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.

Nice!

Comment on lines +408 to +410
# TODO: no error
# error: [static-assert-error]
static_assert(not constraints.implies_subtype_of(TypeOf[identity2], Callable[[str], int]))
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a significant issue -- is there a next step / plan for how to fix this TODO?

Copy link
Member Author

Choose a reason for hiding this comment

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

We will need to alpha-rename the typevars that we add to the inferable set when checking generic signatures. I'll add a TODO

Comment on lines +1249 to +1250
static_assert(is_assignable_to(TypeOf[identity], Callable[[int], int]))
static_assert(is_assignable_to(TypeOf[identity], Callable[[str], str]))
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, why do we do better in these tests than in the subtype-of tests below? I don't see any gradual forms, so I would think they should have the same results.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's because of these lines:

(_, Type::TypeVar(typevar))
if typevar.is_inferable(db, inferable)
&& relation.is_assignability()
&& typevar.typevar(db).upper_bound(db).is_none_or(|bound| {

I can remove the is_assignable part of the test, and it does cause the subtype checks to line up with the assignability checks here. But it causes other tests to start failing, because this interacts poorly with the union specialization heuristics here:

(Type::Union(formal), _) => {
// Second, if the formal is a union, and precisely one union element is assignable
// from the actual type, then we don't add any type mapping. This handles a case like
//
// ```py
// def f[T](t: T | None): ...
//
// f(None)
// ```
//
// without specializing `T` to `None`.

Did I mention that the current setup is very brittle? 😅

I can add a TODO in the code and here calling out that this is a known inconsistency we're working towards removing

Comment on lines -641 to -649
// The typevars in self and other should also be considered inferable when checking whether
// two signatures are equivalent.
let self_inferable =
(self.generic_context).map(|generic_context| generic_context.inferable_typevars(db));
let other_inferable =
(other.generic_context).map(|generic_context| generic_context.inferable_typevars(db));
let inferable = inferable.merge(self_inferable.as_ref());
let inferable = inferable.merge(other_inferable.as_ref());

Copy link
Contributor

Choose a reason for hiding this comment

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

Was this just wrong? Or unnecessary

I don't see any new/changed tests above regarding equivalence of callables, but apparently removing this doesn't break any tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

Bad copy/pasta. I did at one point think that we only needed to force-to-inferable the typevars of one side of the callable comparison, thinking we needed that to handle a co/contravariance thing. But that's already being handled correctly by how we perform the recursive subtyping calls below, so I think it's correct to mark both sides as inferable. (This is a mumbly explanation since much of that detail has paged out of my memory at this point)

But in the world where that hunch was correct, then we wouldn't do any force-to-inferable marking here, because if we only mark one side in has_relation_to, and is_equivalent_to is akin to has_relation_to in both directions, then neither side is always forced-to-inferable. (That inconsistency was another hint that made me think my first hunch was wrong.)

dcreager and others added 7 commits November 14, 2025 18:17
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
* origin/main: (59 commits)
  [ty] Improve diagnostic range for `non-subscriptable` diagnostics (#21461)
  [ty] Improve literal promotion heuristics (#21439)
  [ty] Further improve details around which expressions should be deferred in stub files (#21456)
  [ty] Improve generic class constructor inference (#21442)
  [ty] Propagate type context through conditional expressions (#21443)
  [ty] Suppress completions when introducing names with `as`
  [ty] Add panic-by-default await methods to `TestServer` (#21451)
  [ty] name is parameter and global is a syntax error (#21312)
  [ty] Fixup a few details around version-specific dataclass features (#21453)
  [ty] Support attribute-expression `TYPE_CHECKING` conditionals (#21449)
  [ty] Support stringified annotations in value-position `Annotated` instances (#21447)
  [ty] Type inference for genererator expressions (#21437)
  [ty] Make `__getattr__` available for `ModuleType` instances (#21450)
  [ty] Increase default receive timeout in tests to 10s (#21448)
  [ty] Add synthetic members to completions on dataclasses (#21446)
  [ty] Support legacy `typing` special forms in implicit type aliases (#21433)
  Bump 0.14.5 (#21435)
  [ty] Support `type[…]` and `Type[…]` in implicit type aliases (#21421)
  [ty] Respect notebook cell boundaries when adding an auto import (#21322)
  Update PyCharm setup instructions (#21409)
  ...
* dcreager/deep-comparison: (64 commits)
  assuming
  SubtypingAssuming
  implies_subtype_of
  name tweak
  Apply suggestions from code review
  [ty] Improve diagnostic range for `non-subscriptable` diagnostics (#21461)
  [ty] Improve literal promotion heuristics (#21439)
  [ty] Further improve details around which expressions should be deferred in stub files (#21456)
  [ty] Improve generic class constructor inference (#21442)
  [ty] Propagate type context through conditional expressions (#21443)
  [ty] Suppress completions when introducing names with `as`
  [ty] Add panic-by-default await methods to `TestServer` (#21451)
  [ty] name is parameter and global is a syntax error (#21312)
  [ty] Fixup a few details around version-specific dataclass features (#21453)
  [ty] Support attribute-expression `TYPE_CHECKING` conditionals (#21449)
  [ty] Support stringified annotations in value-position `Annotated` instances (#21447)
  [ty] Type inference for genererator expressions (#21437)
  [ty] Make `__getattr__` available for `ModuleType` instances (#21450)
  [ty] Increase default receive timeout in tests to 10s (#21448)
  [ty] Add synthetic members to completions on dataclasses (#21446)
  ...
Base automatically changed from dcreager/deep-comparison to main November 14, 2025 23:43
* origin/main:
  [ty] Implement constraint implication for compound types (#21366)
static_assert(constraints.implies_subtype_of(CallableTypeOf[identity], Callable[[int], int]))
static_assert(constraints.implies_subtype_of(CallableTypeOf[identity], Callable[[str], str]))
static_assert(not constraints.implies_subtype_of(CallableTypeOf[identity], Callable[[str], int]))
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I tried to see if tests like the following would pass here …

type GenericIdentity[T] = Callable[[T], T]
type GenericFunction[T, R] = Callable[[T], R]

static_assert(constraints.implies_subtype_of(TypeOf[identity], GenericIdentity))
static_assert(constraints.implies_subtype_of(CallableTypeOf[identity], GenericFunction))

… but I get a panic due to a failing debug assertion

Copy link
Member Author

Choose a reason for hiding this comment

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

I traced this a bit, and the issue is that type_alias.value_type is returning a dynamic callable for these type aliases. I think that's a separate issue from this PR, though I will add some logic here to make this not cause a panic!

Copy link
Member Author

Choose a reason for hiding this comment

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

If you do

static_assert(constraints.implies_subtype_of(TypeOf[identity], GenericIdentity[int]))

the test passes. value_type is eagerly specializing the type alias to GenericIdentity[Unknown]. So other than the panic, this might be the correct expected behavior?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is correct/expected that a not-explicitly-specialized use of a generic type alias in a type expression is implicitly default-specialized.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed the panic and added these as additional tests

@dcreager dcreager merged commit e4a32ba into main Nov 17, 2025
41 checks passed
@dcreager dcreager deleted the dcreager/coolable branch November 17, 2025 18:43
dcreager added a commit that referenced this pull request Nov 17, 2025
* main:
  [ty] Constraint sets compare generic callables correctly (#21392)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal An internal refactor or improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants